github mastra-ai/mastra @mastra/core@1.75.0
October 7, 2026

5 hours ago

Highlights

Span Query API (list completed spans across traces)

New core/server/client support lets you query individual completed spans (with filters, cursors, previews, and model cost) via storage.querySpans() / client.querySpans() / POST /api/observability/spans/query, enabling workflows like “show every failed tool_call in the last hour” without first finding traces.

Trace Aggregation with token + cost analytics (now across stores)

aggregateTraces() now supports token and cost measures (tokens.* sums/avgs and cost.sum/cost.avg with coverage + currency handling), and is implemented in the ClickHouse, DuckDB, and Postgres observability stores—making cost/tokens-first dashboards and “top spenders” queries a first-class capability.

Self-embedding vector stores for Semantic Recall (no client embedder required)

Semantic recall can now run against vector stores that generate embeddings themselves (MastraVector.isSelfEmbedding), including MongoDBVector via autoEmbed, so memory recall and message writes work without configuring a client-side embedder.

@mastra/connect 1.0: new channels/providers capabilities (Teams, Discord credential security) + better observability

@mastra/connect reaches stable with a reworked provider API (providers allowlists, glob filters, top-level defaults, dynamic tool merging), adds Microsoft Teams to channels(), moves Discord to a single API-key bot-token credential stored in encrypted credentials, and now records connect tool calls as trace spans for end-to-end observability.

Sessions now persist one active model per thread + optional “no initial thread”

AgentController sessions now keep a single current model across mode switches (updated switch APIs and thinking-level persistence), and you can start sessions without creating a thread (createInitialThread: false / createSession({ createInitialThread: false }) + session.thread.ensureId()), reducing empty-thread clutter.

Breaking Changes

  • @mastra/connect@1.0.0: discovered MCP tools no longer require approval by default; use requireApproval (replaces autoApproveTools) to opt in.
  • @mastra/connect: API rename integrations → providers; removed legacy surfaces like the connect() alias of tools(), environment() sandbox credential surface, and several resolver helpers/exports.
  • Slack channel integration id changed in channels() from slack → slack-channels (Slack tools remain on slack).
  • AgentController model switching: removed modeId/scope; session.model.switch signature changed to switch(modelId, options?) and sessions persist one model per thread (mode switches no longer auto-change models).
  • @mastra/react hooks: positional arguments replaced with a single object argument; all hooks accept queryOptions (TanStack Query), and hooks no longer guard on empty ids by default.

Changelog

@mastra/core@1.75.0

Minor Changes

  • Added the StorageMemoryRef type so a stored agent's memory field can reference a registered memory instance with { type: 'id', memoryId }, or hold an inline config. Existing inline configs are still accepted. (#25850)

    Also in: @mastra/client-js@1.52.0, @mastra/server@1.75.0

  • Added support for several processes sharing one notification store, where only some of them can run a given thread. (#25741)

    dispatchDueNotifications accepts a resourceId to dispatch only that resource's due notifications. A delivery policy can return hold: true to leave a due notification pending for another dispatcher, without counting a delivery attempt.

    hold only applies when a due notification is dispatched. When a notification is first sent, return an action that does not deliver it, such as defer:

    import { defaultNotificationDeliveryDecision } from '@mastra/core/notifications';
    
    const agent = new Agent({
      // ...
      notifications: {
        deliveryPolicy: {
          decide: input =>
            canRunHere(input.record.resourceId)
              ? defaultNotificationDeliveryDecision(input)
              : { action: 'defer', deliverAt: input.now, hold: true },
        },
      },
    });
  • Added an explicit reconcile step for channel installations whose connect flow finishes outside the app (for example Discord's bot invite, which never redirects back). Channel providers can implement the new optional reconcileInstallation(agentId) method, exposed over POST /api/channels/:platform/:agentId/reconcile and client.channels.reconcileInstallation(platform, agentId). The route requires the same write access as connecting, and returns the agent's fresh installation — or null when the platform doesn't support reconciliation. Listing installations is now a pure read and never changes state. (#25993)

    const installation = await client.channels.reconcileInstallation('discord', 'my-agent');
    // { id, platform, agentId, status: 'active', ... } once the invite completed

    Also in: @mastra/client-js@1.52.0, @mastra/server@1.75.0

  • Added optional title, websiteUrl, and icons options to MCPServerConfig, exposed on MCPServerBase, so MCP server implementations can announce a display title, website, and icons to clients. (#25985)

  • Added token and cost measures to aggregateTraces(). Requests can now ask for tokens.input, tokens.output, tokens.total, tokens.reasoning, and tokens.cached (each as .sum or .avg), plus cost.sum and cost.avg, and use them in having and orderBy. (#25735)

    Usage is summed per trace first, then per group. Averages are per trace with token usage, so traces with no recorded usage don't pull them down. Any request for a cost.* measure also returns cost on each row: cost.coverage is the share of traces with token usage whose cost was fully priced, and cost.unit is the currency. When a group mixes currencies, cost.sum and cost.avg are null and cost.unit is "mixed". Groups whose measure is null sort last and never satisfy a having condition.

    import { parseTraceAggregateRequest, planTraceAggregate } from '@mastra/core/storage';
    
    const plan = planTraceAggregate(
      parseTraceAggregateRequest({
        timeRange: { from: '2026-06-01T00:00:00Z', to: '2026-09-01T00:00:00Z' },
        groupBy: ['entityName'],
        interval: '1d',
        measures: ['count', 'tokens.input.sum', 'tokens.output.sum', 'cost.sum'],
        orderBy: { field: 'cost.sum', direction: 'desc' },
        limit: 20,
      }),
    );
    
    const { rows } = await storage.aggregateTraces(plan);
    // rows[0].measures → { count, 'tokens.input.sum', 'tokens.output.sum', 'cost.sum' }
    // rows[0].cost → { coverage: 0.96, unit: 'usd' }

    TokenMetrics, the names of the token usage metrics, is now exported from @mastra/core/observability.

  • Semantic recall now works with a vector store that generates embeddings itself, so a self-embedding store needs no client-side embedder. (#25009)

    Added isSelfEmbedding to MastraVector. It defaults to false, so every existing store is unaffected. A store that embeds text itself overrides it to return true, and semantic recall against that store needs no embedder:

    const memory = new Memory({
      storage,
      vector: new MongoDBVector({ id: 'vec', uri, dbName, autoEmbed: { model: 'voyage-4' } }),
      options: { semanticRecall: true },
    });

    A configured embedder still takes precedence, so adding one to the example above returns to client-side embedding.

    Messages embedded by the store are kept in an index named memory_messages_selfembed, separate from the indexes holding client-supplied vectors.

    Configuring semantic recall with neither an embedder nor a self-embedding store now says so, naming both options.

  • Added a storage API for querying individual completed spans with filters, cursors, bounded previews, and model cost. (#25791)

    Shared cursor, timeout, and resource-limit errors now say "query" so they also describe span queries accurately. Error codes are unchanged.

    import { planSpanQuery } from '@mastra/core/storage';
    
    const result = await storage.querySpans(
      planSpanQuery({
        timeRange: { from: '2026-10-01T00:00:00Z', to: '2026-10-02T00:00:00Z' },
        where: { op: 'eq', left: { path: 'spanType' }, right: { literal: 'tool_call' } },
      }),
    );
  • Changed AgentController sessions to keep one active model instead of a separate model for each mode. Switching modes no longer changes the model automatically, and session.model.switch no longer accepts modeId or scope. The model_changed event no longer includes modeId or scope; consumers should read its modelId and current thinkingLevel fields. When an existing thread is first reopened, its active legacy per-mode selection is copied to currentModelId, a migration marker is stored, and obsolete modeModelId_* keys are removed. Later model selections persist as the thread's authoritative currentModelId. Use session.model.set for an in-memory selection that should not be persisted or emit a model-change event. (#25997)

    Before

    await session.model.switch({ modelId: 'openai/gpt-5.6', modeId: 'build' });
    await session.mode.switch({ modeId: 'plan' }); // Restored the plan mode model.

    After

    await session.model.switch('openai/gpt-5.6');
    await session.mode.switch({ modeId: 'plan' }); // Keeps openai/gpt-5.6 active.
  • Added traceId to agent stream chunks, next to runId. Clients that call /stream can now link a run to its trace without switching to /generate. The field is undefined when tracing is disabled. Custom data-* chunks (for example, from writer.custom()) are left exactly as written and don't include traceId. Fixes #25811. (#25927)

    const stream = await agent.stream('hi');
    for await (const chunk of stream.fullStream) {
      console.log(chunk.runId, chunk.traceId);
    }
  • Added a createSession() option to start a session without creating a thread, and session.thread.ensureId() to create one on first use. Sessions that are never used no longer leave empty threads behind. Sending a message or signal creates the thread automatically. The default is unchanged: sessions still get a thread when none matches. (#22561)

    const session = await controller.createSession({ createInitialThread: false });
    
    // Returns the current thread, or creates one. Concurrent calls share one thread.
    const threadId = await session.thread.ensureId();
  • Added the per-provider object form to the ObservationalMemoryActivationTTL type, so activateAfterIdle accepts values like { default: 'auto', anthropic: '1h' }. New ObservationalMemoryActivationTTLValue and ObservationalMemoryActivationTTLByProvider types are exported. (#25727)

  • Added an outputValidation option to createTool (#23799). When a tool's result fails outputSchema validation, Mastra replaces the result with a validation error, so the model is told the call failed. That happens even when the tool's side effect (an order placed, a message sent) has already happened, which invites a retry. Set outputValidation: 'warn' to return the tool's actual result unchanged instead. 'strict' is the default and keeps the existing behavior. (#26001)

    export const placeOrderTool = createTool({
      id: 'place-order',
      description: 'Places an order',
      inputSchema: z.object({ sku: z.string(), quantity: z.number() }),
      outputSchema: z.object({ orderId: z.string(), total: z.number() }),
      outputValidation: 'warn',
      execute: async ({ sku, quantity }) => orders.create({ sku, quantity }),
    });

    Also improved how output validation failures are reported:

    • Every output validation failure now writes a warning to the Mastra logger. Previously, a failure from a createTool tool left no trace in the logs.
    • The tool call trace span is now marked as failed when a createTool tool returns a validation error. Previously it was recorded as successful.
    • Sensitive fields such as apiKey, token, and password are now redacted from the tool output shown in the validation error message.
  • Changed session.model.switch to accept a model ID followed by an optional options object. Pass { thinkingLevel } to apply and persist model and thinking level together. Every model_changed event includes the current thinking level, even when it is unchanged. Sessions synchronize both preferences from persisted thread settings before the next request, including thinking-only changes and removed overrides. Switches canceled by a thread change before any selection is committed reject without counting model use; writes already committed to the captured thread remain successful. (#26069)

    // Before
    await session.model.switch({ modelId: 'openai/gpt-5.5' });
    await session.state.set({ thinkingLevel: 'high' });
    
    // After
    await session.model.switch('openai/gpt-5.5', { thinkingLevel: 'high' });

Patch Changes

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

  • Fixed MCP resource reads dropping mimeType and _meta. Reading a resource from a server registered through MCPClient (MCPClientServerProxy.readResource()) and reading an app resource from a local MCPServer (MCPServer.readResource(), used by Studio) now return the same metadata as listResources() and the MCP resources/read request, so MCP App ui:// resources keep their content type and UI settings such as CSP. Fixes #23068. (#25992)

    Also in: @mastra/client-js@1.52.0, @mastra/mcp@2.2.0, @mastra/server@1.75.0

  • Fixed a process crash with TypeError: Invalid state: ReadableStream is locked after a provider errored mid-stream. The crash happened when an agent had output processors, such as BatchPartsProcessor or RegexFilterProcessor, and a retrying error processor, such as StreamErrorRetryProcessor. Output processors no longer carry buffered parts across the retry. A stream that is already being read now reports through onError instead of raising an unhandled rejection, which on Node 22 exited the process and dropped every in-flight run. (#25803)

  • Fixed subscribeToThread({ withInitialHistory: true }) returning thread-history messages newest first. The initial history now lists the newest page of messages oldest first, matching listThreadMessages and the documented behavior. (#25821)

  • Added an optional scope field to AuthorizeOpts so tool providers know whether a new connection is shared, per-author, or caller-supplied. (#26002)

  • Fixed workspace search reporting the wrong lineRange when a custom tokenizer preserves case. Highlighting now uses the same tokens as retrieval, so searching Python points at the line containing Python rather than a line containing python. (#26014)

  • Fixed three cases where a durable agent's stream, or a regular agent's saved approval metadata, did not match what the stream reported. (#26006)

    • Tool result chunks now include providerExecuted, which tells the caller whether the tool ran on the provider's side. It was missing on durable runs, so clients received undefined where regular agents report the value.
    • Approval requests on durable agents now publish the same resumeSchema as regular agents, including $schema, additionalProperties: false and the field descriptions. The accepted resume data is unchanged.
    • Approval metadata saved by regular agents now carries the same resumeSchema as the live approval request, including the optional reason field the saved copy was missing. The accepted resume data is unchanged.
  • Fixed dataset schema updates validating items against a previously proposed schema, and fixed items being validated against the old schema after changing the schema of an empty dataset. (#25659)

  • Fixed model call spans that listed tools the model did not receive. Each MODEL_INFERENCE span now has a tools attribute with the tool definitions (name, description, parameters) sent to the provider on that call, after input processors, prepareStep, activeTools, and toolChoice are applied. availableTools on the same span now matches it: it is empty when toolChoice is 'none', and it no longer lists names that are not registered tools. (#25917)

  • Fixed directly resumed nested workflows so successful children continue suspended parent runs. Fixes #24588. (#25817)

  • Fixed call-time clientTools being dropped by durable agents (such as Inngest agents) when the workflow worker runs in a different process from the caller. Client tool schemas are now carried on the workflow input and restored when the worker rebuilds the run's tools, so the model can call them as expected. (#25664)

  • Fixed approval ordering for tools added by input processors, such as ToolSearchProcessor. Approving one call now releases the next call's approval request instead of leaving the run stuck. (#25639)

    Fixed foreach behavior for all evented workflows. Evented workflows now stay suspended until unfinished iterations resume, and queued iterations start only when capacity is available.

  • Fixed DurableAgent traces after a resume or crash recovery. The resumed or recovered agent run is now nested under the original agent run, so the trace stays a single tree instead of splitting into two root spans. (#25862)

    Crash recovery traces are also complete now: the agent run left open by the stopped process is ended with an interrupted status, and the recovered agent run is ended when the run finishes. Previously a recovered trace could be missing from the trace list or show only the part before the crash.

  • Fix durable and evented tool approvals so each approval resumes its own tool call and the agent finishes after the last approval (#25831)

  • Persisted channel thread history as individually attributed messages, including attachments and a marker for omitted messages, instead of one text block on first mention. (#25717)

  • Fixed execute_command and get_process_output results for commands stopped by an aborted run. The result now says the command was aborted instead of showing only a kill exit code such as Exit code: 128, which agents mistook for a real command failure. This covers foreground commands, background processes killed when the run that started them is aborted, and processes killed while get_process_output was waiting on them. (#25752)

    get_process_output on a background process that exited without printing anything now returns its exit status (for example Exit code: 0) instead of (no output yet), so it no longer looks like the process is still running.

  • Fixed fetchWithRetry ignoring cancellation. An already-aborted signal now rejects immediately, and aborting during a request or backoff delay stops further retries instead of waiting through the remaining delays. Retries for non-aborted requests are unchanged. (#26164)

  • Removed the @experimental annotation from Agent goal APIs (goal config, setObjective, GoalSignalProvider) now that goals are stable. (#25941)

  • Preserved extra fields in tool results passed to model messages, client callbacks, and loop callbacks. Client onOutput and toModelOutput callbacks now receive results containing only a value field as that field's value. AI SDK error outputs are stored as failed invocations, skipped by client callbacks, and exposed to response consumers as their underlying error value. (#25680)

  • Fixed pausing, clearing, or replacing a goal while its judge is running being silently undone. The judge's verdict is now discarded when the objective changed during evaluation, so the agent stops instead of continuing a goal you already stopped. (#26093)

  • Fixed goal.scorer so a string resolves a registered scorer by its id, as documented. Previously it was looked up by registration key, so a scorer registered as scorers: { testsPass } with id tests-pass failed with "Scorer with tests-pass not found" and paused the goal. Registration keys still work as a fallback. (#26154)

  • Fixed agent runs failing when the goal judge's feedback signal could not be streamed (for example, because a durable agent's transport had closed). The verdict is still recorded and the run finishes normally. (#25665)

  • Resource read results from MCP servers now include the optional mimeType and _meta fields, both in the MCPServerBase.readResource() type and in the POST /mcp/:serverId/resources/read response returned to readMcpServerResource(). (#25992)

    Also in: @mastra/client-js@1.52.0, @mastra/mcp@2.2.0, @mastra/server@1.75.0

  • Fixed executor interruptions so default-engine workflow runs remain recoverable instead of being recorded as canceled. Fixes #24586. (#24684)

  • Removed the @experimental annotation from Agent signal APIs (sendSignal, subscribeToThread, sendMessage, queueMessage, cancelQueuedMessages, abortThread, state and notification signals, and signal providers) now that signals are stable. (#25946)

    Also in: @mastra/client-js@1.52.0

  • Fixed a leak where an output stream processor that throws let the original, unprocessed chunk through. For example, a redaction processor that crashed would still stream the text it was meant to redact. (#25826)

    Now streamed content chunks processed inside regular, durable, and evented runs stop before the failing chunk is emitted, emit an error chunk, and finish with finishReason: 'error'. Durable tool-chunk processor failures also fail their workflow step before the tool chunk is published. For durable and evented agents, failures on lifecycle chunks such as start, step-start, step-finish, and finish error fullStream and reject derived output promises. For regular agents, step-start and finish failures emit an error chunk and resolve with finishReason: 'error', while a step-finish failure errors the stream; start is not passed to output stream processors. Previously the processor error was logged and the unprocessed chunk was emitted while the stream finished with finishReason: 'stop'. abort() in a processor still blocks the chunk as before.

    const result = await agent.stream('Reveal the card number');
    
    try {
      for await (const chunk of result.fullStream) {
        if (chunk.type === 'error') {
          console.error(chunk.payload.error); // e.g. "redactor crashed"
        }
      }
    } catch (error) {
      // Some lifecycle-chunk processor failures error the stream directly.
      console.error(error);
    }
  • Added internal groundwork for querying individual spans across traces. (#25701)

  • Fixed durable threads that reported suspended tool calls as lost after a restart. Pending tool calls remain available for a response. (#25764)

  • Fixed DurableAgent and EventedAgent runs to fail and report the error when mapping a server-executed tool result, including an awaited background result, throws in toModelOutput. Client-executed tools and completed deferred background tasks continue with the raw result when their mapper throws. (#25762)

  • Fixed evented agents hanging forever when workflow workers run in a separate process (for example, an API server started with MASTRA_WORKERS=false plus a dedicated worker deployment). Runs now reach the worker process and stream back to the caller. Values used in more than one place in a workflow now keep their data when sent between processes instead of arriving as null. A process without workers now logs a warning instead of silently dropping a workflow it can only run locally. (#26036)

  • Fixed sendToolApproval to preserve separate approval decisions when custom resume data is provided. Approval-gated calls now reject custom resume data unless it is a non-null object that can carry the decision. (#25632)

  • Fixed stream(..., { untilIdle: true }) repeating earlier output when a background task triggers a follow-up turn. The supplied runId can abort the initial or follow-up turn through abortRunStream(runId) or abortThreadStream(...). (#25661)

  • Fixed agents running out of memory when untilIdle is set in defaultOptions. agent.stream() and agent.resumeStream() now wait for background tasks once instead of looping endlessly before calling the model. Fixes #26043. (#26125)

  • Fixed skill publishing so it rejects JavaScript frontmatter (---js) in SKILL.md instead of running it. Publishing now parses SKILL.md the same way as loading and validating a skill. Fixes #25551. (#25689)

  • Fixed agent runs stopping with "Interrupted" when an Observational Memory reflection didn't compress enough on its first try. The reflector retries at a stronger compression level, but each retry was reported as a failure, and the agent controller cancelled the run before the retry could finish. Retry attempts are now marked as retrying, so only a final failure stops the run. (#26105)

    Also in: @mastra/memory@1.36.0

  • Fixed structured output failing with 400 errors on OpenAI-compatible providers (for example Azure AI Foundry through @ai-sdk/openai-compatible with supportsStructuredOutputs: true). These providers send a strict JSON schema, but Mastra only prepared schemas for strict mode when the provider name started with openai, so requests failed with errors like 'uniqueItems' is not permitted or Missing 'subject'. Schemas are now prepared whenever the model sends a strict JSON schema, and null values for optional fields are accepted in the response. Setting strictJsonSchema: false in the provider options keeps the schema unchanged. (#25707)

  • Fixed requests failing with empty assistant content after switching models mid-thread, such as from an OpenAI reasoning model to Claude. When reasoning the new provider cannot accept is removed from history, assistant turns that contained only that reasoning are now left out instead of being sent empty. (#23050)

  • Fixed batched channel messages from other senders skipping custom onMention, onDirectMessage, and onSubscribedMessage handlers when burst, debounce, or queue concurrency is set. (#25854)

    Your handler now runs once for each sender's turn, with its own ctx.requestContext, ctx.signalMetadata, and ctx.skipped. A handler can now be called several times for one dispatch.

  • Cancelling a run while structured output is being generated no longer logs a false "Structured output processing failed" error. The structuring model call is now cancelled along with the run, and no output chunks are written after the run's stream has closed. (#25662)

  • Fixed restart() for evented workflows that crashed while resuming a nested workflow. The resumed step now keeps its resume data and completes on restart instead of suspending again. Fixes #25365. (#25630)

  • Fixed a thread failing on every turn when one of its attachments can't be used: a file that now returns 404, a relative path such as /api/images/foo.png, or inline content that isn't valid file data. If error processors and fallback models don't recover a failed download, the agent now answers with an [Attachment unavailable: <name>] placeholder and logs a warning. The attachment is recorded on its stored message, so later turns reuse the placeholder without downloading it again. See #23705. (#25051)

  • Fixed aborting a thread run that had several messages sent to it: the follow-up run now answers all of them in one turn, instead of one message per run (which made you abort once per pending message). Messages added with queueMessage still run one at a time after it. (#25749)

  • Fixed durable and evented agent terminal behavior. Aborted runs now emit an abort chunk, report finishReason as aborted, and call onAbort instead of onFinish. Failed runs call onError instead of onFinish. resume(), observe(), and recover() now accept an onAbort callback. (#25765)

  • Fixed ResponseCache sharing cached responses between users who are identified only through memory: { resource }. When no scope is set, the cache now falls back to the memory resource ID after the auth resource ID, so each user gets their own cache entries. Set scope: null to keep sharing responses across users. (#26192)

  • Fixed error-processor retries being dropped when a run reached its maxSteps limit. An agent run with maxSteps: 1 that hit a transient 429/5xx would previously resolve as a successful empty (or partial) response with finishReason: 'retry'. Retries now re-run the failed step without counting against maxSteps, and if every retry fails the run ends with finishReason: 'error' and the last provider error (including fields like statusCode) on output.error and onError, on the default, durable, and evented engines. (#25804)

  • Fixed streamed PIIDetector output with the block and filter strategies. When a Social Security number, email address or similar value arrived split across several stream chunks, the first part of it reached the user before the rest was recognized. Now block stops the response before any part of the value is shown, and filter removes the text up to and including the value while keeping the text after it. Very long email addresses are now also held until they are complete, including with redact. As with redact, the end of a response may arrive slightly later. (#26082)

  • Fixed fresh-process durable agent resumes replaying suspended output when cached stream history is unavailable. (#25760)

  • Fixed usage aggregation so omitted provider token counts remain unknown across agent, workflow, and durable streams. Reported cache and reasoning details remain additive. (#25402)

    AccumulatedUsage from @mastra/core/agent/durable and WorkflowDataPart['data']['output']['usage'] from @mastra/ai-sdk now represent incomplete primary counters as undefined instead of measured zeroes. When every input and output count is known, totalTokens can still be derived from those complete aggregates even if the provider omitted its total. Derived totals now sum input and output tokens without adding reasoningTokens.

    For example, { inputTokens: 10, outputTokens: 20, totalTokens: 30 } followed by { outputTokens: 5 } now produces { inputTokens: undefined, outputTokens: 25, totalTokens: undefined }.

    Observability still records known per-step token contributions and marks their aggregate as incomplete. TokenCostControl treats the partial estimated cost as a known lower bound: hard and soft thresholds still apply when that lower bound crosses them, while lower values do not imply the complete cost is under budget. The Responses API returns usage: null for an incomplete aggregate instead of fabricating zero-valued counters.

    Durable iteration state written by this version may omit unknown primary counters and cannot be resumed by an older worker after a rollback. (#23469)

    Also in: @mastra/ai-sdk@1.10.7

  • Fixed thread streams treating a run as finished while it was still waiting for a tool approval or a suspended tool. When thread updates were delivered with a delay (for example with @mastra/redis-streams), the thread could unblock and subscribers on other servers could lose the pending approval. The run now stays waiting until the approval or tool resumes it. (#25962)

  • Added optional rules to prompt_block_ref instruction blocks so stored agents can save and preview per-usage display conditions on prompt block references. (#25991)

    await client.getStoredAgent('support-agent').update({
      instructions: [
        {
          type: 'prompt_block_ref',
          id: 'default-user-prompt',
          rules: { operator: 'AND', conditions: [{ field: 'userPrompt', operator: 'not_exists' }] },
        },
      ],
    });

    Also in: @mastra/client-js@1.52.0, @mastra/react@1.8.0, @mastra/server@1.75.0

  • Fixed processToolResult aborts in output processors so they end the run at the first aborted tool result. Previously, aborting with parallel tool calls crashed the run with Controller is already closed, and aborting a single tool call made an extra model call after the stream had ended. (#26157)

  • Fixed ToolNotFoundError when approving a tool loaded by ToolSearchProcessor with storage: 'context' after the run resumes without in-process state — for example a DurableAgent resuming after a restart or after its run registry entry expired, or resumeStream landing on a different instance. The resumed run now rebuilds the loaded tools from the thread's persisted messages. (#25666)

  • Tool input validation errors now include the path of each invalid field. Previously, MCP 1.x servers returned errors like Invalid input: expected array, received string with no indication of which argument was wrong. Errors now read, for example: (#25794)

    - items.0.tags: Invalid input: expected array, received string
    - options.destination: Invalid input: expected string, received undefined
    

    Fixes #25766.

  • Fixed two restart problems on durable runs. (#26035)

    Recovering a suspended durable agent run now fails immediately with a clear message pointing at resume(). Previously it resolved, then emitted a lone "This workflow run was not active" error. Continue a run that is suspended on a tool call or an approval with resume(runId, ...).

    Restarting an evented workflow in a process that has not started its workers now starts them before the restart is published. Previously the restart was published to no one, so the run stalled forever with no error, and starting the workers later could not revive it. This does not change instances configured with workers: false (or MASTRA_WORKERS=false). Those still publish the restart to the broker, so the run completes only if another worker consumes it.

  • Fixed agents sending an invented . user message before a conversation that starts with an assistant message, such as a voice agent that greets first. Mastra now adds it only for Amazon Bedrock and Google Gemini, which reject assistant-first conversations. OpenAI, Anthropic, Groq and other providers get the conversation unchanged. The message is added by the default ProviderHistoryCompat processor, after your input processors run, and is still not saved to memory. Fixes #22874. (#25074)

    MessageList prompt getters (such as messageList.get.all.aiV5.prompt()) no longer add the . message, and the ensureGeminiCompatibleMessages helper is removed. If you build prompts from a MessageList yourself and send them to Bedrock or Gemini, add the user turn before a leading assistant message:

    const prompt = messageList.get.all.aiV5.prompt();
    const first = prompt.findIndex(message => message.role !== 'system');
    if (prompt[first]?.role === 'assistant') {
      prompt.splice(first, 0, { role: 'user', content: '.' });
    }
  • Added captureThreadRunTerminalPublish() to detect when an open thread continuation has delivered terminal output. (#25759)

  • Fixed onIterationComplete feedback delivery when the default agent loop has finished. (#25761)

  • Fixed Workspace search indexing creating an extra chunk at the end of large files that only repeated content from the previous chunk. (#26166)

  • Fixed cron schedules whose cadence runs out, such as year-pinned crons. (#25654)

    • The final occurrence now fires once, and the schedule then moves to completed.
    • Editing or resuming a schedule with no future occurrence now completes it instead of failing, whatever status the row held.
    • An invalid cron expression or timezone now returns an input error (400) instead of a server error.
    • ScheduleStatus now includes completed, so code that switches over every schedule status needs a branch for it.

@mastra/ai-sdk@1.10.7

Patch Changes

  • Fixed a client's own auth header being sent to the model provider. When a request body included modelSettings.headers with credentials such as Authorization (for example, an app forwarding its user's bearer token), that header was passed to the provider and overrode the apiKey configured on the server. Agent routes in @mastra/server and handleChatStream/chatRoute in @mastra/ai-sdk now drop credential headers (authorization, proxy-authorization, x-api-key, api-key, x-goog-api-key, cookie) from client-supplied modelSettings.headers. Other headers and body options are passed through unchanged. (#25548)

    Also in: @mastra/server@1.75.0

@mastra/auth-firebase@1.1.3

Patch Changes

  • Updated firebase-admin to v14 and switched to its modular API (firebase-admin/app, firebase-admin/auth). v14 drops node-forge, which has an unfixed signature-forgery advisory (CVE-2026-85393). No change to how you configure MastraAuthFirebase. (#25829)

@mastra/auth-okta@0.2.4

Patch Changes

  • MastraRBACOkta now calls the Okta groups API directly with fetch instead of going through @okta/okta-sdk-nodejs. It pages through the Link header and only follows links on your Okta org. Behavior is unchanged, but the package no longer pulls in node-jose and node-forge, which has an unfixed signature-forgery advisory (CVE-2026-85393). (#25829)

@mastra/braintrust@1.3.19

Patch Changes

  • Bumped braintrust to ^3.36.0. Older braintrust 3.x releases pulled in simple-git 3.36.0, which has critical command-injection advisories (GHSA-x6jw-m9v5-85vh, GHSA-v5rq-49vh-5v5c) that are only fixed in simple-git 4. braintrust 3.36 no longer depends on simple-git. No API changes. (#25966)

@mastra/clickhouse@1.23.0

Minor Changes

  • Added aggregateTraces() support to the ClickHouse observability store. The store now advertises the trace-aggregate capability. It returns grouped counts, error rates, duration statistics, and time-bucketed series over the same traces that queryTraces() selects. Retried and replaced trace roots are collapsed when the query runs, so results stay correct before background merges finish. (#25842)

    const plan = planTraceAggregate(
      parseTraceAggregateRequest({
        timeRange: { from: '2026-08-01T00:00:00Z', to: '2026-08-08T00:00:00Z' },
        groupBy: ['entityName'],
        interval: '1d',
        measures: ['count', 'errorRate'],
      }),
    );
    const { rows, truncated } = await observability.aggregateTraces(plan);

    queryTraces() and aggregateTraces() on ClickHouse are also faster for filters on trace fields such as entityName, status, or metadata.*. These filters now narrow the traces read before duplicate and replaced roots are collapsed, so selective queries over long time ranges do less work. Results are unchanged.

  • Added filtered span queries to ClickHouse v-next observability storage. Queries select matching span identities before loading display fields, previews, and model cost. (#25791)

    const result = await observabilityStorage.querySpans(plan);

Patch Changes

  • Fixed the order of the features the ClickHouse observability store reports, so it matches the Postgres and DuckDB stores. (#25934)

  • Fixed updateMessages deleting and re-inserting every message whose content it updated. Content updates are now applied in place, which is faster and no longer logs the full message content. (#25984)

  • Added token and cost measures to aggregateTraces() in the ClickHouse observability store. (#25970)

    • Token measures: tokens.input, tokens.output, tokens.total, tokens.reasoning, and tokens.cached, each as .sum or .avg.
    • Cost measures: cost.sum and cost.avg. Rows for cost requests also include cost: { coverage, unit }.
    const plan = planTraceAggregate(
      parseTraceAggregateRequest({
        timeRange: { from: '2026-08-01T00:00:00Z', to: '2026-09-01T00:00:00Z' },
        groupBy: ['entityName'],
        measures: ['tokens.total.sum', 'cost.sum'],
      }),
    );
    const { rows } = await observability.aggregateTraces(plan);
    // rows[0] → { dimensions: { entityName: 'support' }, measures: { 'tokens.total.sum': 7800, 'cost.sum': 3.75 }, cost: { coverage: 0.75, unit: 'usd' } }

    Usage comes from the model token metrics of each trace. Retried metric writes count once without waiting for background merges, and spend recorded after the window ends still counts for traces that started inside it.

  • Improved ClickHouse query performance by letting filters use table indexes instead of scanning whole tables. (#25715)

    Observability

    • Delta polling and list cursors for traces, branches, logs, metrics, scores and feedback now read only the rows past the cursor instead of the whole signal table.
    • Score dashboards and listScores filtered by trace no longer merge the entire current-scores table.
    • Trace and branch list pages and paginated trace queries use far less memory and load faster. Listing branches no longer runs out of memory on large deployments.
    • Metric, score and feedback percentile charts compute all requested percentiles in one query.
    • Added skip indexes for trace-root and branch lookups by trace, feedback lookups by id, and log filters by trace, thread, resource, user, organization, experiment, run, session and request.

    Memory, workflows and scores

    • updateMessages and updateResource are much faster on large tables because they no longer rewrite the entire table on every call.
    • saveMessages, updateMessages and listThreads respond faster, especially for batches of messages and for users with many threads.
    • Added skip indexes for message, thread, resource, workflow run and score lookups by id.

    Skip index coverage

    The new skip indexes are added automatically on the next init(). They cover data written from then on immediately; existing data is covered only as ClickHouse merges it. Older memory and workflow data rarely re-merges, so lookups over existing history stay as slow as before until the index is built for it. To cover existing data now, run this once per table and index (a background job that reads the indexed column):

    ALTER TABLE <table> MATERIALIZE INDEX idx_<column>;

    For example, ALTER TABLE mastra_messages MATERIALIZE INDEX idx_thread_id;. List the indexes on a table with SELECT name FROM system.data_skipping_indices WHERE table = '<table>'.

  • Fixed listTraces failing with a SQL syntax error when filtering by hasChildError in the ClickHouse observability store. (#25984)

@mastra/client-js@1.52.0

Minor Changes

  • Added the optional liveKitRecordingRouteEnabled capability to getSystemPackages() response types so applications can discover recording review support. (#24674)

    const packages = await client.getSystemPackages();
    if (packages.liveKitRecordingRouteEnabled) {
      // Show recording controls for voice call traces.
    }
  • Added an optional thinking level to controller session switchModel calls. (#26069)

    // Before
    await session.switchModel('openai/gpt-5.5');
    
    // After
    await session.switchModel('openai/gpt-5.5', { thinkingLevel: 'high' });
  • Added a typed querySpans() method that lists completed spans matching a span filter, one row per span, with cursor pagination. (#25920)

    const result = await mastraClient.querySpans({
      timeRange: { from: '2026-10-01T00:00:00Z', to: '2026-10-02T00:00:00Z' },
      where: { op: 'eq', left: { path: 'spanType' }, right: { literal: 'tool_call' } },
      page: { limit: 50 },
    });
    // { spans: [...], page: { next } }

    queryTraces() with a spans.some filter returns the traces that contain a matching span. querySpans() returns the matching spans themselves. Check capabilities.spanQuery from getObservabilityCapabilities() before you call it.

  • Removed the options argument from AgentController switchModel. Model selection now persists to the active thread and remains active when its mode changes. (#25997)

    Before

    await controller.switchModel('openai/gpt-5.6', { modeId: 'build', scope: 'thread' });

    After

    await controller.switchModel('openai/gpt-5.6');
  • Added skills.sh registry methods to the workspace client: searchSkillsSh, listPopularSkillsSh, previewSkillsSh, installSkillsSh, removeSkillsSh and updateSkillsSh. Parameters and responses are typed from the server routes. (#25700)

    const workspace = client.getWorkspace('my-workspace');
    const { skills } = await workspace.listPopularSkillsSh({ limit: 10 });
    await workspace.installSkillsSh({ owner: 'vercel-labs', repo: 'skills', skillName: 'find-skills' });

Patch Changes

  • Added completed to the schedule status types. A schedule whose cron has no future occurrence reports completed when fetched by id, and the schedules list includes completed schedules only when you filter by status: 'completed' — the default listing omits them. status on a schedule update still only accepts active or paused. (#25654)

@mastra/cloudflare@1.7.1

Patch Changes

  • Fixed listWorkflowRuns({ status }) failing the whole query with malformed JSON when any stored snapshot was nested deeper than SQLite's JSON depth limit. Such snapshots are now skipped by the status filter, and other runs are returned normally. (#26131)

    Also in: @mastra/cloudflare-d1@1.4.1

@mastra/code-sdk@1.11.0

Minor Changes

  • Made the controller runtime independent of Mastra Code model packs. Hosts can now provide a generic modelRoute for fallback models, account routing, and observational-memory models. Removed disableModelPacks and activeModelPackId from the controller runtime state. (#25997)

    Before

    const code = await createMastraCode({ disableModelPacks: true });
    await session.state.set({ activeModelPackId: 'anthropic' });

    After

    const code = await createMastraCode();
    await session.state.set({
      modelRoute: {
        entries: [
          { id: 'primary', label: 'Primary', modelId: 'openai/gpt-5.6' },
          { id: 'fallback', label: 'Fallback', modelId: 'anthropic/claude-fable-5' },
        ],
      },
    });
  • Added a createInitialThread option to createMastraCode(). Set it to false to start without a thread; the first message creates one, so a session that's never used leaves no empty thread behind. The default is unchanged. (#22561)

    createMastraCode({ createInitialThread: false });
  • Agents in different projects on the same machine can now find each other. With cross-agent communication on (crossAgentSignals: true or the signals.experimentalCrossAgentSignals setting), agent_connections_list lists Mastra Code instances in every project, and agent_connect and agent_signal_send reach them. Before, the list only showed instances in the same project. (#25758)

    import { createMastraCode } from '@mastra/code-sdk';
    
    const mastraCode = await createMastraCode({
      unixSocketPubSub: true,
      crossAgentSignals: true,
    });

    An instance whose resource ID (from MASTRA_RESOURCE_ID or .mastracode/database.json) can't be used as a directory name, for example one containing /, stays out of cross-project discovery and logs one warning at startup.

    Any local process running as the same user can discover and message advertised agents. Set MASTRACODE_SIGNALS_SOCKET_ROOT to an absolute path to keep an instance or a test away from the shared /tmp/mc directory.

    Also fixed signal sockets piling up under /tmp/mc. Every agent discovery request used to leave an open socket, and often a socket file, behind, so long-running instances could accumulate thousands. One-off discovery replies now close as soon as they're answered. Headless mastracode --prompt runs also close their signal sockets on exit instead of leaving the files behind.

  • Editors that connect to Mastra Code over ACP (Agent Client Protocol), such as Zed and JetBrains IDEs, can now sign you in without leaving the editor. (#25871)

    • Sign in with a ChatGPT subscription, Kimi For Coding, or xAI in the browser, started from the editor.
    • Editors that support terminal sign-in also get Claude and GitHub Copilot sign-in, plus mastracode-login for any other provider or an API key. Each runs mastracode login in a terminal.
    • Starting a session with no configured provider returns an auth_required error, so editors show these sign-in options instead of a session with no usable model.
    • New sessions start on a model you can use. If the default model's provider isn't configured, the session switches to the default model of a provider you signed in to.
    const { authMethods } = await connection.initialize({
      protocolVersion: PROTOCOL_VERSION,
      clientCapabilities: { auth: { terminal: true } },
    });
    // openai-codex, kimi-for-coding, xai, anthropic, github-copilot, mastracode-login
    
    await connection.authenticate({ methodId: 'openai-codex' });

Patch Changes

  • Fixed Unix socket path handling so a thread ID can no longer place a socket outside its resource directory, whether or not cross-project agent discovery is enabled. Thread IDs that would reuse the lease or discovery socket names are rejected as well. (#25876)

  • Observational memory and thread-title calls on Anthropic no longer write their whole prompt to the 1-hour prompt cache. Each call sends different conversation content, so that cache entry was never read. They now cache only their shared instructions for 5 minutes, which the next observer call reads. The main agent still uses the 1-hour cache. (#25739)

  • Fixed Mastra Code starting a second agent run on a thread that was already running in another project's Mastra Code process. Every local Mastra Code process shares one database, so any of them could pick up a notification meant for another project's thread, see that thread as idle, and run it with its own session and workspace. (#25741)

    Mastra Code processes now coordinate a thread's runs no matter which project they belong to, so a busy thread stays busy for every process. Each process delivers the due notifications of its own sessions' resources itself, and only one process delivers a resource's notifications at a time, so a notification is never run by a process that cannot own its thread.

  • Improved ACP session shutdown by waiting up to two seconds for pending notification dispatch and stopping workers before closing session resources. (#25875)

  • Fixed ACP model selection to apply the model and its compatible thinking level together. (#26069)

  • Mastra Code now caches prompts for 1 hour instead of 5 minutes on direct Anthropic connections (API key or Claude subscription), and waits for 1 hour of idle time before activating buffered observations on those models. Claude through other providers, such as Amazon Bedrock, keeps the 5-minute behavior. Coming back to a conversation after a break of up to an hour reuses the cached prompt instead of resending it in full. (#25727)

    Cost tradeoff: writing a 1-hour cache entry costs 2× the base input price, compared with 1.25× for a 5-minute entry. Cache reads cost the same. Sessions that pause for between 5 minutes and an hour can come out cheaper because they read the cache instead of rewriting it. Sessions that never pause that long pay more for every cache write.

    The 1-hour cache also applies to Anthropic models used for memory observation, reflection, and thread titles, since they go through the same model setup.

  • Bumped the ajv pin in the ACP validator script to 8.20.0. No runtime changes. (#25816)

  • Bumped probe-image-size to ^7.4.0 in @mastra/memory to pick up the fix for a quadratic-time denial of service in its SVG parser (GHSA-gjj5-9665-rwrc). Bumped the smol-toml pin in @mastra/code-sdk from 1.8.0 to 1.9.0 for a parser security fix. No API changes. (#25966)

    Also in: @mastra/memory@1.36.0

@mastra/connect@1.0.0

Major Changes

  • Breaking: Discovered MCP tools no longer require approval by default, matching @mastra/mcp's own default. Opt into approval per integration with the new requireApproval option. (#25740)

    Previously, every MCP tool discovered through connect() was forced to require approval, with an autoApproveTools escape hatch to list specific keys that should skip it. The forced-approval policy made the typical case needlessly interactive and diverged from @mastra/mcp, whose default is "no approval unless the server definition opts in."

    The new requireApproval option replaces autoApproveTools and applies to generated HTTP toolsets as well as discovered MCP tools:

    • Omit (or pass false) → no approval required for any tool on this provider
    • true → every tool on this provider requires approval
    • string[] → approval required only for the listed tool keys

    Unknown names in the array fail resolution with an invalid_options error instead of silently dropping the provider, so a typo can neither widen access nor remove the toolset. A config that still contains the removed autoApproveTools key throws at tools() call time with a migration hint, so loosely typed configs cannot carry the dead option forward and run previously-gated tools without a prompt.

    If you relied on the old always-on default (for example, agents scaffolded from the connect templates), opt back in explicitly with requireApproval: true on the providers whose tools should prompt before running.

    Migration

    // Before: approval required by default, allowlist of tools that skip it
    connect({
      integrations: {
        neon: { autoApproveTools: ['neon_list_projects', 'neon_describe_project'] },
      },
    });
    
    // After (conservative): gate every tool on the provider. This preserves the
    // former approval policy, except the tools previously listed in
    // autoApproveTools now prompt too.
    connect({ integrations: { neon: { requireApproval: true } } });
    
    // After (targeted): gate only the listed tools. Not equivalent to the old
    // default; every tool missing from the list runs without a prompt, so list
    // every tool that must stay gated.
    connect({
      integrations: {
        neon: { requireApproval: ['neon_delete_project'] },
      },
    });

Minor Changes

  • Reworked the @mastra/connect API ahead of the first stable release. Breaking: the package has not shipped stable, so no deprecation period applies. (#26084)

    Renamed integrations to providers

    The option on tools() and channels() is now called providers, with types renamed to match (ToolsProviderOptions, ChannelsProviders, ChannelsProviderOptions).

    // Before
    const connectTools = tools({ integrations: { linear: { allowTools: ['linear_get_issue'] } } });
    
    // After
    const connectTools = tools({ providers: { linear: { allowTools: ['linear_get_issue'] } } });

    Merge your own tools in a dynamic tools callback

    Spread the resolver's result alongside local tools; your tools win on key collision.

    const agent = new Agent({
      // ...
      tools: async ctx => ({ ...(await connectTools(ctx)), weatherTool }),
    });

    A .with() convenience method also exists for the same merge (tools: connectTools.with({ weatherTool })); it accepts a static record or a (sync/async, optionally context-reading) function, and calls chain.

    Simpler provider selection

    The array form is now a real allowlist (only the listed providers resolve), and the record form accepts boolean shorthand:

    tools({ providers: ['linear', 'resend'] }); // only these resolve
    tools({ providers: { linear: true, github: false } }); // enable / exclude

    Unknown provider ids now fail with invalid_options instead of silently resolving nothing. channels() throws at call time; tools() checks ids against the platform catalog when it resolves tools, and if the catalog can't be reached it logs a warning and skips the id.

    In channels(), slack is accepted as an alias for the platform's slack-channels key, so the common spelling needs no quotes:

    channels({ providers: { slack: true, discord: true } });

    Both spellings configure the same channel; naming it twice throws.

    Glob filters and top-level defaults

    allowTools, disallowTools, and requireApproval accept * globs, and all three can be set at the top level of tools() as defaults for every provider. Per-provider options win, including requireApproval: false to opt out of a global policy.

    tools({
      requireApproval: ['*_delete_*', '*_send_*'],
      providers: { linear: { requireApproval: false } },
    });

    A per-provider glob that matches nothing fails like an unknown literal name, so typos never silently widen access.

    Removed

    • The deprecated connect() alias of tools().
    • The environment() sandbox credential surface.
    • The MASTRA_<PROVIDER>_CONNECTION_ID env-var pin is no longer documented (it keeps working); pass connectionId per provider instead.
    • The TOOLS registry alias (use PROVIDERS) and the findRegistration/findChannelRegistration lookup helpers.
    • Internal toolset plumbing (defineProxyTool, applyAllowTools, resolveConnectionId and their types) is no longer exported.
    • The disabled: true per-provider field — use the false shorthand instead: providers: { github: false }.
    • The resolver invalidate() method — call refresh() to force a fetch now, or lower ttlMs to control freshness.
  • Discord (channel and generated tools) now targets the platform's discord integration as a single API-key connection whose credential is the bot token, sourced from the encrypted /credentials endpoint on every call. The same connection powers both the channel and every generated tool from one credential. (#25358)

    Why: connection metadata is treated as non-secret by the platform, so a bot token stored there was readable by any caller with project access. Every Discord surface now reads the bot token from the platform's encrypted, audited credentials path.

    Breaking:

    • The 33 generated Discord tools no longer read the bot token from connection metadata; they call getConnectionWithCredentials() and use credentials.apiKey.
    • The discord integration on the platform is now API-key auth (Nango's discord-bot provider), not OAuth. OAuth-typed credentials on the channel are skipped with a warning — Discord rejects OAuth user tokens for bot authentication.

    Migration:

    1. Reconnect the discord integration on the platform against the new API-key auth flow (Nango's discord-bot) and paste the bot token when prompted. The token is now stored in the encrypted credential, not connection metadata.
    2. Remove any botToken you were still writing to connection metadata — it is no longer read and, since metadata is non-secret, it should not be left behind.

    Before:

    // Bot token was pulled from non-secret connection metadata.
    // Tools called `getMetadata<{ botToken: string }>()` internally.

    After:

    import { channels, tools } from '@mastra/connect';
    
    const projectId = 'proj_...';
    
    // Generated Discord tools — bot token now comes from the encrypted
    // `credentials.apiKey` on the `discord` connection.
    const toolResolver = tools({
      projectId,
      integrations: { discord: { connectionId: 'conn_...' } },
    });
    const discordTools = await toolResolver();
    
    // Discord channel — same `discord` connection, same credential.
    const channelResolver = await channels({
      projectId,
      integrations: {
        discord: { providerOptions: { commandScope: 'global' } },
      },
    });
    const providers = await channelResolver(); // providers.discord
  • Improved error handling across all providers and updated the Clerk tools, verified by a new live end-to-end test suite that exercises every tool of all 26 checked-in providers. (#25815)

    Clearer error messages — errors returned as { errors: [{ message, long_message }] } (Clerk, Linear, and other providers) now surface the human-readable message instead of a generic failure.

    Fixed error recovery in generated tools — MastraConnectError now also exposes its HTTP status as error.response.status. Generated tools check this field in their error handlers (for example the create-vs-update fallback in github_create_or_update_file), which previously never matched and failed instead of recovering.

    Updated Clerk tools (regenerated from the upstream template fix, NangoHQ/integration-templates#670):

    • clerk_list_sessions now requires client_id or user_id and no longer returns total.
    • clerk_create_user now requires at least one identifier (email_address, phone_number, or username).
    • clerk_list_users now returns an accurate total.
    // Before: accepted by the schema, then rejected by the Clerk API
    await toolset.clerk_list_sessions.execute({ status: 'active' });
    
    // After: the input schema requires a client or user filter
    await toolset.clerk_list_sessions.execute({ user_id: 'user_123', status: 'active' });
  • Added Microsoft Teams to channels() and re-keyed the Slack channel from slack to slack-channels. (#25332)

    Microsoft Teams: projects with an active microsoft-teams platform connection now resolve a TeamsProvider automatically — no bot credentials in your code. Connecting an agent provisions a dedicated Teams bot for it through the platform connection. Requires MASTRA_ENCRYPTION_KEY (a 32-byte value, base64-encoded) in the server environment: the Teams install store persists each provisioned bot's client secret at rest, so channels() refuses to construct the Teams provider without it and skips the integration with a warning until a key is configured.

    Breaking: Slack channel integration id renamed. Use the slack-channels connection to hook the Slack channel up to agents; the existing slack connection continues to back the generated Slack tools. Two separate connections, one purpose each.

    If you pass per-integration overrides to channels(), migrate the key:

     channels: await channels({
       projectId: process.env.MASTRA_PROJECT_ID,
       integrations: {
    -    slack: { providerOptions: { defaultChannel: 'C123' } },
    +    'slack-channels': { providerOptions: { defaultChannel: 'C123' } },
       },
     }),
    import { Mastra } from '@mastra/core/mastra';
    import { channels } from '@mastra/connect';
    
    const mastra = new Mastra({
      agents: { myAgent },
      channels: await channels({ projectId: process.env.MASTRA_PROJECT_ID }),
    });
    
    // With `microsoft-teams` and `slack-channels` connections active, both
    // channels resolve automatically and agents can be connected from Studio
    // or via the API. Teams additionally requires MASTRA_ENCRYPTION_KEY set
    // in the server environment.
  • Connect tool calls now show up in traces. When observability is enabled, every platform API call and proxied vendor call appears as a child span under the tool call span, recording the method, a safe route template, connection id, and response status. Spans never include query strings, headers, bodies, or vendor path segments; apps without observability configured are unaffected. (#26080)

  • Add Google Analytics (GA4) provider to @mastra/connect with 11 generated tools from the Nango integration-templates upstream: archive-conversion-event, batch-run-reports, create-conversion-event, create-data-stream, create-property, get-metadata, run-pivot-report, run-realtime-report, run-report, update-data-stream, and update-property. (#25743)

    Opt in by setting MASTRA_GOOGLE_ANALYTICS_CONNECTION_ID and selecting google-analytics in the integrations map.

Patch Changes

  • Fixed Jira issue creation so agents can discover project-specific issue types and their required fields. (#26119)

  • @mastra/connect now requires @mastra/core 1.75.0 or later, matching the @mastra/mcp version it depends on. (#25985)

@mastra/convex@1.7.1

Patch Changes

  • Added support for recording a schedule's terminal status in the same atomic update that claims its final firing. A schedule whose cron has no future occurrence is now stored as completed instead of staying active, so its last occurrence is delivered once instead of on every tick. (#25654)

    Upgrade this store alongside @mastra/core. A store that predates this argument leaves the row active, and the scheduler re-fires that final occurrence on each tick.

    Also in: @mastra/libsql@1.25.1, @mastra/mongodb@1.22.0, @mastra/mysql@0.12.1, @mastra/pg@1.30.0, @mastra/spanner@1.9.2

@mastra/deployer@1.75.0

Patch Changes

  • Fixed mastra build on Yarn 1 (classic) workspaces leaving workspace-module/ empty. Workspace dependencies are now packed into the build output, so installing the output no longer fails. (#25788)

@mastra/discord@1.3.0

Minor Changes

  • Fixed Discord installs staying "pending" forever after completing the bot invite. Discord's invite flow doesn't notify the server when it finishes, so the provider now exposes reconcileInstallation(agentId) — it activates a pending install when the server can attribute the newly joined guild to it, and Studio calls it when you return, so the agent shows "Connected" right away: (#25993)

    const info = await discord.reconcileInstallation('my-agent');
    // info?.status === 'active' once the invite finished

    When the new guild can't be attributed safely (several invites in flight, or the bot joined more than one guild), the install stays pending and activates on its first interaction, as before. Invite attribution expires after 30 minutes and never crosses a bot credential change. listInstallations() is now a pure read.

Patch Changes

  • Fixed a Discord Gateway reconnect storm that could trip Discord's connection abuse limit (>1000 connects in a short window) and get the bot token force-reset. The provider now owns the Gateway reconnection loop: failed connects (invalid token, Message Content privileged intent not enabled) back off exponentially instead of retrying instantly, a revoked token parks reconnection until new credentials arrive, and exactly one loop runs per installation — credential rotations and disconnect() now stop the previous loop instead of leaking it. (#26095)

@mastra/docker@0.9.3

Patch Changes

  • Fixed Docker sandbox process output corrupting non-ASCII characters. Emoji, CJK, and accented text in stdout/stderr no longer turn into � when a character spans Docker stream frames — streamed output, handle.stdout/handle.stderr, and wait() results now contain the original text. (#26015)

  • Fixed a spurious kill(...) failed unexpectedly warning when cancelling a command that had just finished on its own. If the command has already exited, kill() now returns false quietly and the normal exit result is kept. Real failures to stop a running command are still logged. (#26179)

@mastra/duckdb@1.13.0

Minor Changes

  • Added filtered span queries to DuckDB observability storage with stable pagination, previews, and model cost. (#25791)

    const result = await observabilityStorage.querySpans(plan);
  • Added aggregateTraces() support to the DuckDB observability store. The store now advertises the trace-aggregate capability and returns grouped counts, error rates, duration statistics, and time-bucketed series over the same traces that queryTraces() selects. (#25835)

    const plan = planTraceAggregate(
      parseTraceAggregateRequest({
        timeRange: { from: '2026-08-01T00:00:00Z', to: '2026-08-08T00:00:00Z' },
        groupBy: ['entityName'],
        interval: '1d',
        measures: ['count', 'errorRate'],
      }),
    );
    const { rows, truncated } = await observability.aggregateTraces(plan);

    queryTraces(), queryThreads(), and trace-query discovery are also faster over large stores: the time range, tenant scope, and simple root filters such as environment, entityName, or threadId now narrow the scan before each trace's current root span is selected. Results are unchanged.

Patch Changes

  • Added token and cost measures to aggregateTraces() in the DuckDB observability store. (#25971)

    • Token measures: tokens.input, tokens.output, tokens.total, tokens.reasoning, and tokens.cached, each as .sum or .avg.
    • Cost measures: cost.sum and cost.avg. Rows for cost requests also include cost: { coverage, unit }.
    const plan = planTraceAggregate(
      parseTraceAggregateRequest({
        timeRange: { from: '2026-08-01T00:00:00Z', to: '2026-09-01T00:00:00Z' },
        groupBy: ['entityName'],
        measures: ['tokens.total.sum', 'cost.sum'],
      }),
    );
    const { rows } = await observability.aggregateTraces(plan);
    // rows[0] → { dimensions: { entityName: 'support' }, measures: { 'tokens.total.sum': 7800, 'cost.sum': 3.75 }, cost: { coverage: 0.75, unit: 'usd' } }

    Usage comes from the model token metrics of each trace. Retried metric writes count once, and spend recorded after the window ends still counts for traces that started inside it.

  • Fixed trace list pages and trace queries that loaded most of the observability table and could fail with an out-of-memory error (#25518). (#25802)

    • listTraces, listTracesLight and listBranches now read only the page's own spans. Oldest-first pages, such as the ones a retention job reads, no longer load the whole table.
    • queryTraces reads only traces whose root spans fall in the requested time range, including when it filters on related spans.

    On a 100k-trace store, the oldest listTraces page went from loading about 1.9 GB of table data to about 32 MB, and queryTraces from about 860 MB to about 25 MB. Results are unchanged.

  • Reduced memory use when reading spans that carry large inputs or outputs (#25518). (#25932)

    DuckDB loads every large value stored near a requested span when those values sit next to empty ones. Fetching one span or one trace page could therefore load hundreds of megabytes. New spans store missing input, output, attributes and requestContext values to avoid this. On a store with large agent payloads, reading one span went from about 316 MB to about 1 MB. The oldest trace page went from about 469 MB to about 145 MB.

    Results are unchanged. Spans written by earlier versions read the same as before and keep the old memory use until they are pruned.

@mastra/editor@0.16.0

Minor Changes

  • Added per-usage display conditions to prompt block references. A prompt_block_ref instruction can now carry its own rules, so an agent can include a shared stored block only in specific situations without adding rules to the shared block itself. If the stored block has rules too, both must pass. (#25991)

    This makes it possible to insert a runtime value from request context and fall back to a shared default block when the value is missing (#17878):

    await mastra.getEditor()!.agent.update({
      id: 'support-agent',
      instructions: [
        { type: 'text', content: 'Follow the platform safety policy.' },
        {
          type: 'prompt_block',
          content: '{{userPrompt}}',
          rules: { operator: 'AND', conditions: [{ field: 'userPrompt', operator: 'exists' }] },
        },
        {
          type: 'prompt_block_ref',
          id: 'default-user-prompt',
          rules: { operator: 'AND', conditions: [{ field: 'userPrompt', operator: 'not_exists' }] },
        },
      ],
    });
  • Added support for Composio shared connected accounts in ComposioToolProvider. Fixes #18959. (#26002)

    Create shared accounts. Set sharedConnections to create Composio SHARED accounts for connections authorized with scope: 'shared'. The optional ACL controls which Composio users can use the account. Without it, Composio's deny-by-default access applies.

    new ComposioToolProvider({
      apiKey: process.env.COMPOSIO_API_KEY!,
      sharedConnections: { acl: { allowAllUsers: true } },
    });

    Select shared accounts. The connection picker now lists SHARED accounts, including accounts another user created in the Composio dashboard when their ACL grants access. Pinned SHARED accounts now report the correct connection status.

    Fixed: Adding a second connection for the same toolkit failed with a multiple connected accounts error. You can connect multiple accounts per toolkit again.

  • Stored agents can now use a memory instance registered on your Mastra instance instead of copying its config. Set memory to { type: 'id', memoryId: '<registry key>' } and the agent uses that exact instance, including its storage, processors, and working memory settings. Many stored agents can share one memory setup. (#25850)

    const mastra = new Mastra({
      memory: { supportMemory },
      editor: new MastraEditor(),
    });
    
    await mastra.getEditor()!.agent.create({
      id: 'support-agent',
      name: 'Support agent',
      instructions: 'Help customers.',
      model: { provider: 'openai', name: 'gpt-5' },
      memory: { type: 'id', memoryId: 'supportMemory' },
    });

    If the key isn't registered, the agent loads without memory and a warning names the missing key. Existing inline memory configs keep working. Cloning an agent that uses registered memory now stores a reference instead of a copy. Fixes #21890.

@mastra/elysia@0.1.15

Patch Changes

  • Fixed createAuthMiddleware dropping the refreshed session Set-Cookie after a transparent session refresh. Raw Elysia routes protected by createAuthMiddleware now send the refreshed session headers on both allowed and denied (401/403) responses, alongside any cookies the route sets through ctx.cookie, set.headers, or a returned Response. Previously the refreshed cookie was lost, which could log users out or revoke sessions that use single-use refresh tokens. (#25796)

@mastra/factory@0.20.0

Minor Changes

  • Replaced the Factory web model-pack preferences API with one personal default model. New interactive chats start with that model, while existing chats keep their selected model. (#25997)

    API consumers must replace calls to /web/config/model-packs with the new default-model routes:

    await fetch('/web/config/default-model', {
      method: 'PUT',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ modelId: 'openai/gpt-5.6' }),
    });

    Use GET /web/config/default-model to read the preference and DELETE /web/config/default-model to clear it.

Patch Changes

  • Fixed Review cards for pull requests that Factory discovers by polling GitHub, instead of through a webhook. These cards now record the pull request author. Previously the author was missing, so the reviewer refused to publish its verdict because it could not match the card to the pull request. (#25792)

  • Fixed issues and pull requests with the same number in two linked repositories collapsing into one Factory card. New GitHub cards are now keyed by repository and number, and reviews only link to pull request cards from their own repository. (#26094)

  • Updated controller model selection to use the model ID and optional options-object API. (#26069)

@mastra/inngest@1.10.3

Patch Changes

  • Fixed resumed Inngest agent runs so approved tool results and final responses continue reaching thread subscribers without replaying already completed output. (#25759)

  • Fixed Inngest agent traces so tracing exporters such as PostHog show every tool called during a run. The MODEL_GENERATION span output now includes the run's toolCalls. (#25819)

  • Fixed sendToolApproval to preserve approval decisions when Inngest agents receive custom resume data. Approval-gated calls now reject custom resume data unless it is a non-null object that can carry the decision, and unresolved approval targets fail instead of resuming without the decision. (#25632)

@mastra/lance@1.5.0

Minor Changes

  • Fixed indexConfig.type: 'ivfflat' creating a product-quantized (IVF PQ) index instead of an IVF Flat index. ivfflat now creates a native LanceDB IVF Flat index, and the new ivfpq type creates the IVF PQ index that ivfflat previously produced. (#26121)

    Indexes created before this release keep their IVF PQ type. Rebuild an index for a type change to take effect.

    To keep IVF PQ, change ivfflat to ivfpq:

    // Before
    indexConfig: { type: 'ivfflat', numPartitions: 128, numSubVectors: 16 }
    
    // After
    indexConfig: { type: 'ivfpq', numPartitions: 128, numSubVectors: 16 }

    To switch to IVF Flat, keep ivfflat, remove numSubVectors, and rebuild the index:

    indexConfig: { type: 'ivfflat', numPartitions: 128 }
  • Added optimize() and getIndexCoverage() to LanceVectorStore so you can keep vector indexes up to date. Rows written after an index is built stay outside the index and slow down queries until the table is optimized. Mastra never optimizes automatically, so you choose when to run it. (#26155)

    const [coverage] = await store.getIndexCoverage({ indexName: 'docs' });
    
    if (coverage.numUnindexedRows >= 1_000) {
      await store.optimize({ indexName: 'docs' });
    }

    Concurrent optimize() calls for the same table share one run. deleteUnverified defaults to false. See #26100.

Patch Changes

  • Fixed LanceVectorStore.createIndex() rebuilding an existing vector index on every call. Calling it again with the same column, index type, distance metric and build settings now reuses the existing index; changed settings or indexes replaced outside Mastra still trigger a rebuild. (#26122)

@mastra/livekit@0.5.0

Minor Changes

  • Raised the supported LiveKit Agents and plugin minimum to 1.7.1 so speech flushing and playback outcome APIs are available. Before: The peer range ^1.4.0 accepted Agents 1.4–1.6. After: The minimum is 1.7.1. Upgrade Agents and any installed LiveKit or Silero plugins to matching versions: (#24674)

    npm install @livekit/agents@1.7.1 @livekit/agents-plugin-livekit@1.7.1 @livekit/agents-plugin-silero@1.7.1

    Existing session setup can stay in place. Attach the playback observer before starting the session:

    import { voice } from '@livekit/agents';
    import { observeVoiceSession } from '@mastra/livekit/plugin';
    
    const session = new voice.AgentSession();
    observeVoiceSession(session, {
      onSpeechComplete: result => console.log(result.outcome),
    });
  • Added a browser-safe @mastra/livekit/client entry point for recording playback. It exports getLiveKitRecording, LiveKitRecording, and LiveKitRecordingResponse, reusing a Mastra client's authentication and fetch configuration without importing LiveKit server or worker code. (#24674)

    Applications using the client entry point need @mastra/client-js 1.51.2 or newer within 1.x. The SDK is an optional peer; server and worker imports do not require it.

    If you used client.getLiveKitRecording(traceId) in an earlier snapshot, replace it with:

    import { getLiveKitRecording } from '@mastra/livekit/client';
    
    const recording = await getLiveKitRecording(client, traceId);

    Studio now uses this entry point for Review Audio.

  • Fixed recording URL validation so malformed URLs produce validation failures without throwing unexpected exceptions. Playback remains limited to HTTP and HTTPS URLs. (#24674)

    Aligned the Zod peer range with LiveKit Agents: ^3.25.76 || ^4.1.8. Older Zod versions are no longer accepted. Upgrade before installing this release:

    npm install zod@^3.25.76
    # Or use Zod 4:
    npm install zod@^4.1.8
  • Added per-turn generation metrics, playback completion hooks, and speech-segment flushing. onTurnComplete keeps its generation-only behavior; onSpeechComplete reports server playback outcomes without changing memory persistence. (#24674)

    // Before: generation completion only
    createLiveKitWorker({ mastra, onTurnComplete: handleGeneration });
    
    // After: observe generation and playback separately
    createLiveKitWorker({
      mastra,
      onTurnComplete: handleGeneration,
      onTurnMetrics: handleMetrics,
      onSpeechComplete: handlePlayback,
    });

    Reply generators can now emit a VOICE_TEXT_FLUSH boundary as well as strings. Custom string-only generators still work. Code consuming generator output directly must handle the boundary explicitly:

    // Before
    text += chunk;
    
    // After
    if (typeof chunk === 'string') text += chunk;
    else flushSpeechSegment();

    For MastraLLM, use the mastraLLMNode adapter in the LiveKit agent to translate these boundaries. Playback transcripts may be partial or estimated; server playback does not prove that the remote listener heard the audio.

Patch Changes

  • Required an explicit recording authorization callback before trace lookup or playback URL resolution. Missing policies fail at route registration, and denied requests cannot access recording storage. (#24674)

    // Before: authentication alone did not check recording ownership.
    liveKitRecordingRoute({ resolveRecording });
    
    // After: the application verifies access to the requested trace.
    liveKitRecordingRoute({ authorize: authorizeRecording, resolveRecording });

    The authorization callback must return true to permit access, including when requiresAuth is false for a local demo.

  • Fixed vad: false to disable voice activity detection when creating a LiveKit session. LiveKit treats an omitted VAD as a request for its bundled default, so the worker now explicitly opts out. Explicit VAD instances, prewarmed Silero, and sessionOptions overrides keep their existing behavior. (#24674)

  • Fixed liveKitConnectionRoute() to return HTTP 409 when recording is requested for an existing room, without dispatching an agent or issuing a participant token. Use a unique roomName for each recorded call. (#24674)

    Programmatic callers of dispatchVoiceSession() can distinguish this conflict with the exported LiveKitRecordingRoomConflictError:

    import { dispatchVoiceSession, LiveKitRecordingRoomConflictError } from '@mastra/livekit';
    
    try {
      await dispatchVoiceSession({ roomName, recording });
    } catch (error) {
      if (error instanceof LiveKitRecordingRoomConflictError) {
        console.error(`Room ${error.roomName} already exists; start the call with a fresh room name.`);
      } else {
        throw error;
      }
    }

@mastra/loggers@1.3.5

Patch Changes

  • HttpTransport no longer buffers logs without limit while its endpoint is unavailable. The buffer is now capped by maxBufferSize (default 10,000 entries); when it is full, the oldest logs are dropped first. Use getDroppedLogCount() to see how many logs were dropped. Only one flush request is sent at a time, so an outage no longer triggers a new failing request for every log written. Destroying the transport now sends every buffered batch before it finishes, not just the first one. batchSize and maxBufferSize must be positive integers; the constructor now throws for values such as 0, Infinity, or 2.5 (leave batchSize unset to use the default of 100). (#25958)

    import { HttpTransport } from '@mastra/loggers/http';
    
    const transport = new HttpTransport({
      url: 'https://logs.example.com/ingest',
      maxBufferSize: 5_000,
    });
    
    transport.getDroppedLogCount(); // number of logs dropped because the buffer exceeded maxBufferSize
  • Fixed HttpTransport retrying logs that the endpoint will never accept. When the endpoint rejects a batch as invalid, such as with a 400 or 413 response, the transport stops retrying right away. It drops that batch and counts it in getDroppedLogCount(), so newer logs are no longer stuck behind it. (#26079)

    Temporary failures are still retried, including network errors, timeouts, rate limits and server errors. If the server sends a Retry-After header, the transport waits that long before retrying, up to the request timeout.

  • FileTransport log queries now read the log file as a stream instead of loading it synchronously, so listLogs() no longer blocks the event loop and paginated queries only keep the requested page in memory. listLogsByRunId() now finds matching logs beyond the first 100 records. (#26191)

@mastra/mcp@2.2.0

Minor Changes

  • Added MCPClient.getServerInfo(), which returns the identity each connected MCP server announced when it connected: its name, version, and, when provided, title, description, website URL, and icons. Use it to show users which server they connected instead of only the URL they entered. A server's entry is undefined if it has not connected yet, or if it connected without announcing an identity, which newer servers are allowed to do. Fixes #24559. (#25849)

    await mcp.listTools();
    
    const info = mcp.getServerInfo();
    console.log(info.myServer?.title, info.myServer?.version, info.myServer?.icons);
  • MCPServer now announces its full identity to MCP clients: title, description, websiteUrl, and icons, alongside name and version. Clients, including MCPClient.getServerInfo(), can show a display title, description, website, and logo for a Mastra server instead of only its name. The registry server info returned by getServerInfo() on the server is unchanged. Fixes #25856. (#25985)

    const server = new MCPServer({
      name: 'weather-server',
      version: '1.0.0',
      title: 'Weather Server',
      description: 'Forecasts and current conditions',
      websiteUrl: 'https://weather.example.com',
      icons: [{ src: 'https://weather.example.com/icon.png', mimeType: 'image/png', sizes: ['48x48'] }],
      tools: { weatherTool },
    });

    @mastra/mcp now requires @mastra/core 1.75.0 or later.

Patch Changes

  • Fixed MCP Apps not opening in hosts that read the app link from a tool call result. MCPServer now returns a tool's _meta.ui.resourceUri (and the flat ui/resourceUri key for older hosts) on successful tools/call results, as it already does on tools/list. getMcpCallToolMeta(result) from Mastra's own client now returns the link. Fixes #21277. (#25990)

    Two related cases change with it:

    • Tools that declare two different links: tools/list and tools/call now both report the nested ui.resourceUri under both keys, so hosts reading either key open the same app.
    • Tools that declare only the flat ui/resourceUri key: the server now advertises the MCP Apps extension for them, as it does for the nested form.

@mastra/memory@1.36.0

Minor Changes

  • recall({ vectorSearchString }) and the message writes behind it now work against a vector store that generates embeddings itself, matching the agent-turn path. With such a store and no embedder configured, saved messages are sent as text and the search string is embedded server-side. (#25009)

    const memory = new Memory({
      storage,
      vector: selfEmbeddingVector,
      options: { semanticRecall: true },
    });
    
    await memory.saveMessages({ messages });
    
    const { messages: recalled } = await memory.recall({
      threadId,
      resourceId,
      vectorSearchString: 'project deadline',
    });

    cloneThread and updateThreadResourceId reach the same path. A cloned thread's messages are embedded for semantic recall, and transferring a thread to another resource moves its message vectors to the new owner so resource-scoped recall keeps finding them.

  • Added per-provider idle activation TTLs to observational memory. Pass an object to activateAfterIdle to set a TTL for specific providers and a default for the rest. Use this when your requests set a prompt cache TTL that 'auto' can't detect, such as Anthropic's per-message cacheControl: { ttl: '1h' }. (#25727)

    Before

    // 'auto' uses 5 minutes for Anthropic: after 5 idle minutes OM activates and rewrites the prompt, invalidating a 1-hour cache that is still warm
    observationalMemory: {
      activateAfterIdle: 'auto',
    }

    After

    observationalMemory: {
      activateAfterIdle: { default: 'auto', anthropic: '1h' },
    }

    Keys match the model's provider before the first . (so anthropic matches anthropic.messages), case-insensitively. Each value takes the same forms as before: milliseconds, a duration string, 'auto', or false. Existing single-value settings behave exactly as before.

Patch Changes

  • Observational memory no longer sends the Observer attachments that the agent recorded as unavailable. The Observer still sees their [Image #1: ...] or [File #1: ...] line in the transcript. See #23705. (#25051)

  • Fixed CPU usage that kept growing when observational memory was created per request. Apps that build a new Memory instance for each request no longer slow down over time on Node.js versions before 24. (#25872)

  • Fixed Observational Memory sending an oversized prompt when a large tool result pushed a step over the messageTokens threshold. Activating buffered observations could leave the newest messages unobserved and still above the threshold, and the next model call could exceed the model's context window (for example, failing with "prompt is too long"). Observational Memory now activates every buffered observation it needs, then observes any remaining messages synchronously when the context is still above the threshold. Fixes #19767. (#25060)

  • Fixed Observational Memory degenerate-output detection so very long lines are truncated and retained, repeated short lines share the existing bounded budget, and genuinely repetitive output remains rejected. (#25212)

    Fixes #24354.

  • Fixed Observational Memory recording completed background-task tool results as null. A null stored model output now falls back to the actual tool result, matching how @mastra/core replays tool results, so the Observer, token counting, and recall see the real result. (#25961)

  • Fixed Observational Memory saving a cut-off or empty observation when the observer or reflector model call failed partway through. This covered streams that ended early (for example Gemini reporting a finish reason of other) and transient provider errors such as a 429 or 500. These calls now retry the whole request from the original prompt instead of continuing from the partial reply, so only complete replies are saved. Fixes #24810. (#25058)

@mastra/mesa@0.3.0

Minor Changes

  • Upgraded to @mesadev/sdk 0.49.1. MesaFilesystem now authenticates with a Mesa private key and mounts a Mesa layout, matching the current Mesa SDK. privateKey falls back to MESA_PRIVATE_KEY when omitted. (#25738)

    Breaking: apiKey, org, and repos are replaced by privateKey, authors, and layout. Paths now start at the layout path instead of /<org>/<repo>.

    Before

    import { MesaFilesystem } from '@mastra/mesa';
    
    const filesystem = new MesaFilesystem({
      apiKey: process.env.MESA_API_KEY,
      org: 'acme',
      repos: [{ name: 'docs', bookmark: 'main' }],
    });
    
    await filesystem.readFile('/acme/docs/README.md');

    After

    import { MesaFilesystem, repo } from '@mastra/mesa';
    
    const filesystem = new MesaFilesystem({
      privateKey: process.env.MESA_PRIVATE_KEY,
      authors: [{ name: 'My Agent', email: 'agent@example.com' }],
      layout: { '/docs': repo('docs', { mode: 'rw', at: { bookmark: 'main' } }) },
    });
    
    await filesystem.readFile('/docs/README.md');

@mastra/mongodb@1.22.0

Minor Changes

  • MongoDBVector takes an autoEmbed config in its constructor and reports itself as a self-embedding store, so Memory's semantic recall can use Automated Embedding with no client-side embedder. (#25009)

    new MongoDBVector({ id: 'vec', uri, dbName, autoEmbed: { model: 'voyage-4' } });

    A createIndex call naming neither its own autoEmbed config nor a dimension picks up those defaults; naming either one overrides them, so one store can hold both kinds of index.

Patch Changes

  • Fixed vector queries failing for a few seconds after a new index is created. While Atlas first builds a vector search index, a query can fail with "cannot query vector index ... while in state INITIAL_SYNC". query now retries across that window instead of throwing, which matters for a caller that queries an index straight after creating it, as semantic recall does on a fresh database. (#25009)

  • Fixed semantic recall returning a message the search had not selected, along with the wrong surrounding context, when several messages share a timestamp. (#25009)

    Messages are ordered by (createdAt, id), but listMessages resolved the message named by include on createdAt alone. Saving messages in one batch gives them the same timestamp routinely, so this was reachable on any query and failed with no error.

@mastra/observability@1.18.4

Patch Changes

  • Fixed stuck spans in Braintrust, LangSmith and PostHog traces. A span that ended right after it started stayed open until shutdown. Shutdown then marked it as failed with 'Observability is shutting down.' This happened, for example, when a tool call failed input validation. These spans now end with their real output. Fixes #26088. (#26136)

  • Added the tools attribute to MODEL_INFERENCE spans. It holds the tool definitions sent to the provider on that call, so exporters can read the tools for each model call. (#25917)

@mastra/pg@1.30.0

Minor Changes

  • Added filtered span queries to PostgreSQL v-next observability storage with stable pagination, previews, and model cost. (#25791)

    const result = await observabilityStorage.querySpans(plan);
  • Added aggregateTraces() support to the PostgreSQL observability store. The store now advertises the trace-aggregate capability and returns grouped counts, error rates, duration statistics, and time-bucketed series over the same traces that queryTraces() selects. (#25722)

    const plan = planTraceAggregate(
      parseTraceAggregateRequest({
        timeRange: { from: '2026-08-01T00:00:00Z', to: '2026-08-08T00:00:00Z' },
        groupBy: ['entityName'],
        interval: '1d',
        measures: ['count', 'errorRate'],
      }),
    );
    const { rows, truncated } = await observability.aggregateTraces(plan);

Patch Changes

  • Fixed standalone storage domains (MemoryPG, WorkflowsPG, ObservabilityPG, and others) ignoring an explicit ssl option when the connection string contains sslmode=. Passing ssl: { rejectUnauthorized: false } now works against servers with self-signed or private-CA certificates, matching PostgresStore. (#26130)

  • Fixed PostgresStore.init() failing with schema "<name>" does not exist when a process connects to more than one database using the same schemaName. Each database now creates its own schema during initialization. (#26135)

  • Fixed PgVector.query() on HNSW indexes for pgvector versions before 0.8.0. On Postgres 15 or later, these queries failed with invalid configuration parameter name "hnsw.iterative_scan". The query now sets hnsw.iterative_scan only on pgvector 0.8.0 or later. (#26013)

  • Fixed $exists filters on nested metadata keys in PgVector. A filter like { 'doc.lang': { $exists: false } } used to match every vector, and $exists: true matched none. Running deleteVectors with such a filter could delete every vector in the selected namespace. Now $exists: true matches vectors where the nested key is present, and $exists: false matches vectors where it is missing. (#25976)

  • Added token and cost measures to aggregateTraces() in the PostgreSQL observability store. (#25967)

    • Token measures: tokens.input, tokens.output, tokens.total, tokens.reasoning, and tokens.cached, each as .sum or .avg.
    • Cost measures: cost.sum and cost.avg. Rows for cost requests also include cost: { coverage, unit }.
    const plan = planTraceAggregate(
      parseTraceAggregateRequest({
        timeRange: { from: '2026-08-01T00:00:00Z', to: '2026-09-01T00:00:00Z' },
        groupBy: ['entityName'],
        measures: ['tokens.total.sum', 'cost.sum'],
      }),
    );
    const { rows } = await observability.aggregateTraces(plan);
    // rows[0] → { dimensions: { entityName: 'support' }, measures: { 'tokens.total.sum': 7800, 'cost.sum': 3.75 }, cost: { coverage: 0.75, unit: 'usd' } }

    Usage comes from the model token metrics of each trace. Retried metric writes count once, and spend recorded after the window ends still counts for traces that started inside it.

@mastra/playground-ui@61.0.0

Minor Changes

  • Improved tool approvals in Factory and Studio. Both now show the decision as a status badge beside the tool name and preview the complete file content before you approve. Pending requests stay expanded so the actions stay visible; once decided, Studio details can collapse without hiding the decision. (#25951)

    Changed ToolApprovalActions now renders only the Approve and Decline buttons and no longer accepts status. Render it only while the request is pending, and show the decision with the new ToolApprovalStatus beside the tool name.

    Before:

    <ToolApprovalActions status={decision} toolName="write_file" onApprove={approve} onDecline={decline} />

    After:

    <>
      <ToolApprovalStatus status={decision} />
      {!decision && <ToolApprovalActions toolName="write_file" onApprove={approve} onDecline={decline} />}
    </>

    ToolApproval renders both parts for you and now accepts args to preview the tool arguments.

    Removed ToolApprovalButtons from @mastra/playground-ui/domains/chat/tools/badges/tool-approval-buttons. Wrap the tool's details in ToolApprovalBadge from @mastra/playground-ui/domains/chat/tools/badges/tool-approval-badge instead: it takes the tool's BadgeWrapper props plus the approval request, shows the status beside the tool name, and keeps the request expanded until it is decided. ToolApprovalButtonsProps is now ToolApprovalRequest.

    Before:

    <BadgeWrapper title="Write file">
      {details}
      <ToolApprovalButtons {...request} />
    </BadgeWrapper>

    After:

    <ToolApprovalBadge title="Write file" approval={request}>
      {details}
    </ToolApprovalBadge>
  • Added useDropZone to turn any element into a native drop target for one drag type. It reports while an accepted drag is over the element and hands the dropped data to your handler. Drags of other types, such as files, pass through untouched. (#25786)

    import { useDropZone } from '@mastra/playground-ui/hooks/use-drop-zone';
    
    const { isDragOver, dropZoneProps } = useDropZone({
      accept: 'application/x-task',
      dropEffect: 'move',
      onDrop: dataTransfer => moveTask(dataTransfer.getData('application/x-task')),
    });
    
    <section className={isDragOver ? 'bg-fill-hover' : undefined} {...dropZoneProps} />;
  • Added thinking-level controls for picking a model's reasoning level. ThinkingLevelPicker is a signal-bars button that sits next to a model picker in a ButtonsGroup and opens a ramp of the model's levels in a popover. When the model has no levels, it shows as disabled with a tooltip that explains why. ThinkingLevelRamp and the compact ThinkingLevelSlider save when you let go, and hold the dropped level until the save finishes. (#25799)

    import { ThinkingLevelPicker } from '@mastra/playground-ui/components/ThinkingLevel';
    
    <ThinkingLevelPicker
      label="Thinking"
      options={[
        { value: 'off', label: 'Off', emphasis: 'muted' },
        { value: 'medium', label: 'Medium' },
        { value: 'high', label: 'High', emphasis: 'warning' },
      ]}
      value={level}
      onChange={setLevel}
    />;
  • Text roles (#25827)

    Added shared headline, introduction, and uppercase section-label roles to Txt:

    <Txt as="h1" variant="hero">Welcome</Txt>
    <Txt variant="lead">Introduction</Txt>
    <Txt as="span" variant="eyebrow">Recent activity</Txt>

    Text elements and component ownership

    Txt supports text elements only; native containers and controls retain their markup, default typography, and child composition.

    Migrate text-only div usages to paragraphs:

    // Before
    <Txt as="div" variant="body">Content</Txt>
    
    // After
    <Txt as="p" variant="body">Content</Txt>

    Keep a native layout wrapper for controls or multiple blocks. Use Code for preformatted code and Txt at the text leaf. Shared components continue to own their typography through existing size and semantic variants, without generic text-role forwarding props. Command groups retain compact uppercase headings.

    Editor fonts

    CodeEditor accepts font="body" for prose editors and defaults to font="mono" for code.

  • Added opensView to sidebar links. A link with opensView: true shows a trailing caret so users can tell it opens its own navigation view. (#25868)

    <Sidebar.Sections
      sections={[{ key: 'infra', links: [{ name: 'Gateway', url: '/gateway', icon: <GatewayIcon />, opensView: true }] }]}
    />
  • Added InputNumber, built on Base UI NumberField, with the same styling as InputGroup. Compose it with optional increment and decrement buttons, decimal steps, and minimum or maximum values. (#25416)

    import {
      InputNumber,
      InputNumberDecrement,
      InputNumberGroup,
      InputNumberIncrement,
      InputNumberInput,
    } from '@mastra/playground-ui/components/InputNumber';
    
    <InputNumber defaultValue={0.7} min={0} max={2} step={0.1}>
      <InputNumberGroup>
        <InputNumberDecrement />
        <InputNumberInput aria-label="Temperature" />
        <InputNumberIncrement />
      </InputNumberGroup>
    </InputNumber>;

    Inside a Field, use FieldLabel and FieldError to name the input and associate validation errors. Existing numeric inputs are unchanged.

  • Added an inset variant to CommandDialog. It wraps the search and results in one panel inside a muted frame, pins the dialog near the top so the input stays put while results change height, and takes a footer for actions like feedback links or keyboard hints. DialogContent also accepts showCloseButton={false} to hide its close button. (#25894)

    <CommandDialog
      variant="inset"
      open={open}
      onOpenChange={setOpen}
      footer={
        <Button variant="ghost" size="sm">
          Send feedback
        </Button>
      }
    >
      <CommandInput placeholder="Search" />
      <CommandList scrollArea scrollAreaViewportClassName="max-h-dropdown">
        {/* groups */}
      </CommandList>
    </CommandDialog>
  • Changed the sidebar More row to work like Linear's. (#25931)

    More menu. More opens a floating menu sized to its content, listing the optional links you moved out of the sidebar, then "Customize sidebar". Opening it no longer pushes the rest of the sidebar down. A link from the More menu appears in place while you are on its page and leaves when you navigate away.

    Customize sidebar. "Customize sidebar" opens a dialog listing every optional link per section. Each link has one of three placements:

    • Always show: stays in the sidebar.
    • Hide in More menu: reachable from More.
    • Never show: hidden from both the sidebar and More.

    Right-click an optional link in the sidebar to change its placement without opening the dialog. Choices save in local storage and survive reloads.

    Pass visibilityStorageKey to Sidebar.Sections to scope saved choices per product, and defaultVisible on an optional link to show it by default. A section with a single optional link keeps showing it without a More row. recentItemsStorageKey is replaced by visibilityStorageKey, and links are no longer promoted automatically after a visit.

    // Before
    <SidebarNew.Sections sections={sections} recentItemsStorageKey="my-app:sidebar" />
    
    // After
    <Sidebar.Sections
      visibilityStorageKey="my-app:sidebar"
      sections={[
        {
          key: 'primitives',
          links: [{ name: 'Agents', url: '/agents' }],
          moreLinks: [
            { name: 'Tools', url: '/tools' },
            { name: 'Workspaces', url: '/workspaces', defaultVisible: true },
          ],
        },
      ]}
    />

    Saved visit history is not migrated. Users can pick placements again through More → Customize sidebar.

  • Added SavedViews to save named filters and page settings in the current browser. Users switch views from tabs, preview their filters, and save or reset edits without losing the selected tab. Failed saves keep edits available to retry. (#25786)

    import { SavedViewEditor, SavedViewTabs, useSavedViews } from '@mastra/playground-ui/components/SavedViews';
    
    const views = useSavedViews({ storageKey: 'my-page.views', settingsSchema, activeViewId, onActiveViewChange });
    
    const setFilters = filters => (views.applied ? views.change({ filters }) : setPageFilters(filters));
    
    <SavedViewTabs views={views} fields={fields} operators={operators} defaultLabel="All" newViewSettings={settings} />
    <SavedViewEditor views={views}>
      <FilterBar fields={fields} value={views.applied?.filters ?? pageFilters} onValueChange={setFilters} />
    </SavedViewEditor>
  • Added headerActionsSlot to TraceSpanPanel and TraceDataPanelView so applications can place actions such as Review Audio alongside the trace controls. Existing panels keep their current behavior when the slot is omitted. (#24674)

    <TraceSpanPanel {...tracePanelProps} headerActionsSlot={<button onClick={openRecording}>Review Audio</button>} />
  • Metrics KPI cards now show the change badge next to the card title, and hovering or focusing it names the window it compares against, such as "vs previous 7d (693)". The window follows the selected time range, including custom ranges, instead of the generic "vs prior period". (#25968)

    MetricsKpiCard.Change now takes a comparison prop for the tooltip text, and the new MetricsKpiCard.Header places it beside the label.

    <MetricsKpiCard.Header>
      <MetricsKpiCard.Label>Agent runs</MetricsKpiCard.Label>
      <MetricsKpiCard.Change changePct={2.6} comparison="vs previous 7d" prevValue="692" />
    </MetricsKpiCard.Header>
  • Added DataList.Group for blocks of rows that share the list's columns but keep their own hover. A group's row lights only while the pointer is on it or in the gap between two of the group's rows. Headings, buttons, empty states and padding around the rows stay dark, and the highlight never jumps to another group. (#25947)

    <DataList columns="auto 1fr auto">
      <DataList.Group aria-label="Open">{openRows}</DataList.Group>
      <DataList.Group aria-label="Closed">{closedRows}</DataList.Group>
    </DataList>
  • Added card-title, card-title-tight, and card-title-strong roles to Txt. Factory card titles keep their existing appearance when developers use these shared styles. (#25784)

    <Txt variant="card-title-tight" tone="ink">
      Review the deployment configuration
    </Txt>
  • Removed the unused outlined tab frame. Contained tabs now use the inset frame by default; explicit frame="inset" remains supported. (#25720)

    Before:

    <Tabs defaultTab="overview" appearance="contained" frame="stroke">
      {children}
    </Tabs>

    After:

    <Tabs defaultTab="overview" appearance="contained" frame="inset">
      {children}
    </Tabs>

    The same frame change applies to TabbedContainer.

  • Added EmptyStateIllustration, line art for empty and error states that animates on hover. Pass it to EmptyState's iconSlot. There are eight for empty screens (traces, logs, api-keys, environments, requests, databases, threads, deploys) and five for failures (disconnected, server-error, locked, not-a-member, rate-limited). tone="error" tints it for failed loads. (#25895)

    <EmptyState
      variant="fill"
      iconSlot={<EmptyStateIllustration name="logs" />}
      titleSlot="No logs yet"
      descriptionSlot="Logs appear here when your project runs."
    />
  • Merged MainSidebar and SidebarNew into one Sidebar component at @mastra/playground-ui/components/Sidebar. Products used to build one sidebar from two overlapping components with different spacing; they now share one API and one look. (#25931)

    Breaking: the @mastra/playground-ui/components/MainSidebar and @mastra/playground-ui/new/sidebar import paths are removed. Import everything from @mastra/playground-ui/components/Sidebar.

    // Before
    import { MainSidebar, MainSidebarProvider } from '@mastra/playground-ui/components/MainSidebar';
    import { SidebarNew, useSidebarNew } from '@mastra/playground-ui/new/sidebar';
    import type { SidebarNewLink, SidebarNewSection } from '@mastra/playground-ui/new/sidebar';
    
    <MainSidebarProvider storageKey="my-app-sidebar" LinkComponent={Link}>
      <SidebarNew>
        <SidebarNew.Sections sections={sections} />
      </SidebarNew>
      <MainSidebar.MobileTrigger />
    </MainSidebarProvider>;
    
    // After
    import { Sidebar, SidebarProvider, useSidebar } from '@mastra/playground-ui/components/Sidebar';
    import type { SidebarLink, SidebarSection } from '@mastra/playground-ui/components/Sidebar';
    
    <SidebarProvider storageKey="my-app-sidebar" LinkComponent={Link}>
      <Sidebar>
        <Sidebar.Sections sections={sections} />
      </Sidebar>
      <Sidebar.MobileTrigger />
    </SidebarProvider>;

    Renames

    • SidebarNew → Sidebar, and every SidebarNew.* part keeps its name: Sidebar.Header, Sidebar.Nav, Sidebar.NavStack, Sidebar.Footer, Sidebar.Meter…
    • MainSidebarProvider and SidebarNew.Provider → SidebarProvider or Sidebar.Provider
    • useMainSidebar and useSidebarNew → useSidebar
    • MainSidebar.MobileTrigger → Sidebar.MobileTrigger
    • MainSidebar.Bottom → Sidebar.Footer
    • Types: NavLink and SidebarNewLink → SidebarLink; SidebarNewSection → SidebarSection; every other SidebarNew*Props → Sidebar*Props
    • data-slot="sidebar-new-*" → data-slot="sidebar-*", for example sidebar-header, sidebar-brand and sidebar-nav-stack-view. Update CSS or test selectors that target them.
    • The default visibilityStorageKey is now mastra:sidebar:link-visibility.

    Collapsed state and width keep their storage keys, so users keep their sidebar layout after upgrading.

  • Added WorkspaceTreeView, a file browser for a workspace with a lazily loaded folder tree, combined file and skill search, and a file viewer for Markdown, syntax-highlighted code, images, and videos. (#25700)

    import { WorkspaceTreeView } from '@mastra/playground-ui/domains/workspace';
    
    const workspace = client.getWorkspace('my-workspace');
    
    <WorkspaceTreeView
      workspaceId="my-workspace"
      activeFilePath={activeFilePath}
      onActiveFileChange={setActiveFilePath}
      onDelete={({ path, type }) => workspace.delete(path, { recursive: type === 'directory' })}
      onCreateDirectory={path => workspace.mkdir(path, true)}
    />;
    • Paths are workspace-relative and . is the root, as on the server.
    • Folder listings and the open file refresh every 10 seconds. The skeleton only shows on the first load.
    • The search icon swaps the tree for a search panel. Typing searches files and skills in parallel.
    • Hovering or focusing a tree row shows the file size. When you pass onDelete, the row also has a delete action. When you pass onCreateDirectory, the header has a "New folder" button. Without these callbacks, the actions don't appear.
    • Errors show a notice that explains the cause, such as an expired session, missing permission, a missing path, or an unsupported operation.
    • Use renderPreview to replace how a file is shown. Return undefined to keep the built-in preview.
    • Markdown files that start with YAML frontmatter (such as SKILL.md) show it as a YAML block above the rendered body. WorkspaceMarkdownPreview and splitFrontmatter are exported so custom renderPreview factories can reuse them.
    • Use asideActions to add your own icon buttons to the aside header.

    The layout is also available as composable Workspace.* parts (Root, Aside, AsideHeader, Search, SearchToggle, CreateDirectory, Tree, ActiveFile, ActiveFileHeader, FilePath, ActiveFileContent).

    Also added FileIcon, TrashIcon, and SearchIcon to the design-system icon set.

    The open file is controlled: the parent owns activeFilePath (state, URL, …) and updates it from onActiveFileChange, which also receives undefined when a delete closes the open file.

    More optional props for embedding the view in a page:

    • readOnlyPaths: folders where delete and new folder are hidden. Pass ['.'] to make the whole workspace read-only.
    • searchFiles / searchSkills: turn each search source on or off. The search button is hidden when both are off.

    The tree also opens the parent folders of the active file and scrolls it into view, shows optional fileCount / skillCount totals in the aside title (for example 3 Skills), and marks mounted folders with their provider icon, a lock when read-only, and an alert when the mount failed.

    Pass addSkill to show an "Add skill" action (in the aside header and the empty state) that opens the skills.sh browser and calls your onInstall handler.

Patch Changes

  • Fixed the default hover highlight, MCP loading text, workspace folder and error icons, and fractional stroke widths on checkbox and switch icons and the workflow time dial. These utilities now generate styles with the current theme. (#24680)

  • Trace and thread loading skeletons now match the loaded layout, so the panels no longer shift when data arrives. The span tree placeholder no longer shows a second search field, its rows have the same height as the real span rows, and the thread placeholder lines up its tabs, messages column and span tree with the loaded thread. (#25812)

    The Messages, Feedback and Scores tabs in the trace panel now appear only once the spans have loaded, so the first tab no longer switches from Feedback to Messages. Rows in the full thread view are no longer dimmed when they are out of view or hovered.

  • FilterBar chips with several values now show the first value and a count, such as prod +2. Hovering the chip or using a screen reader still gives every value. (#25786)

    Fixed chip text copying [object Object] from hidden form inputs.

  • Fixed responsive layout classes being overridden in apps that load @mastra/playground-ui/style.css before their own styles. The workflow graph no longer loads a second copy of the stylesheet, so classes like lg:block and lg:gap-x-32 apply again. (#25902)

  • Fixed search fields and other form fields collapsing to a few pixels wide when placed inside the new sidebar navigation. (#25986)

  • Fixed search inputs overflowing narrow toolbars. (#25786)

  • Fixed the ChatShell composer bouncing with the transcript when you scroll past either end in Chrome, vertically or sideways. Nothing shows through behind or beside the composer anymore: the transcript fades out above it. The scroller also keeps the same room on both sides (--chat-edge, 0.5rem by default), so its scrollbar never overlaps the composer and the column stays centred. (#25592)

  • Added a shared sent-attachment component and Storybook examples for images, PDFs, text, and spreadsheets in Studio and Factory chat. (#25921)

  • Fixed three issues in Command lists: (#25751)

    • Clicking a disabled item no longer selects the item the hover highlight moved to.
    • A Command inside a dialog no longer paints over the dialog's outline.
    • CommandItem only sizes and colors its own icons, so icons inside a Badge or other nested element keep their color when the row is selected.
  • Fixed uneven spacing in RelativeTimestamp when it sits inside a sentence such as "Deployed 1h ago". Only short labels like "1h" stay monospace; words like "ago" and full dates now match the surrounding text. (#25867)

  • Opening the full thread from a trace now shows the thread in its own drawer stacked above the trace, instead of replacing it. The trace stays visible underneath, and "Back to trace" or Escape closes only the thread drawer. TraceThreadPanel now takes open and an optional depth prop, and onBack was removed in favor of onClose. (#25812)

    The "Open full thread" button now sits in the trace side column's tab row, next to Messages, Feedback and Scores, instead of above the conversation.

    When the trace side column is 500px wide or less, the Messages, Feedback and Scores tabs and the "Open full thread" button collapse to icons, with tooltips on hover and keyboard focus. Tab now accepts an optional tooltip prop.

  • Improved sent attachment cards in chat with readable filenames, file-type icons, image previews, and PDF and text dialogs. Attachments appear above the message bubble in Studio and Factory and share its borderless surface and subtle shadow. Image previews have a light inner highlight and progressive blur, with a mask that keeps captions readable in both themes. (#25922)

  • Fixed default buttons blending into cards. A default Button now uses the same fill and edge as the inputs beside it in light and dark themes, so it stands out on a card, settings group, or dialog instead of matching it. Selects and inputs inside a ButtonsGroup now show a single line at each seam instead of two. (#25755)

  • Bumped the jsdom dev dependency to ^30.1.1 for tests. No runtime changes. (#25830)

    Also in: @mastra/react@1.8.0

  • "Highlight spans" in the trace panel now only highlights the spans behind a message: it no longer opens the span detail panel, and it scrolls the last highlighted span into view. (#26064)

  • Fixed MessageScroller with autoScroll not following the first reply in a new conversation. (#25592)

  • Trace panel columns (messages, trace, span) are now resizable down to a minimum width; the messages column is wider by default. In the thread view, the span detail column can also be resized, up to half the width. The divider in data panel headers is now decorative and no longer announced as a separator by screen readers. (#25790)

  • Improved KPI labels to use the shared card-heading typography without changing their appearance. (#25773)

  • Form text and destructive red now match across controls: (#25756)

    • Select and Combobox show the picked value in medium weight, matching text typed into an Input.
    • Validation messages under a field are 13px, the same size as the label and value, instead of 12px.
    • red-400 moves onto the same lightness step as the other hues, so destructive text in dark mode reads as red instead of salmon.
    • Destructive badges use the same light text as the other status badges, so their label stays readable on the red tint.
    • Metrics KPI values drop from semibold to the medium weight of the title role, so no text in the design system goes above 500.
  • Composer attachments now show filenames in consistently sized, compact cards with tighter bottom spacing that stays consistent when scrolling. Remove controls appear on hover or keyboard focus and stay visible on touch devices. Studio shows attachments inside the composer with the same spacing as Factory. (#26061)

  • Fixed trace views for recovered DurableAgent runs. A span that a crashed process left open now shows an "Interrupted" notice instead of being displayed as a regular result. (#25862)

  • Added React Query hooks for the Mastra client, grouped by domain under @mastra/react/hooks/<domain> (for example agents, workflows, traces, metrics, datasets, memory, mcps, tools). React apps can

Don't miss a new mastra release

NewReleases is sending notifications on new releases.