github mastra-ai/mastra @mastra/core@1.78.0
October 15, 2026

4 hours ago

Highlights

Agent Scopes (multi-tenant identity + safer memory/thread routing)

Agents now support scopes (<type>:<value>) passed per call, configured on the agent, or injected by trusted middleware via MASTRA_SCOPES_KEY; Mastra merges all three. Memory can derive resource and thread from resource:/thread: scopes (with conflicts now returning HTTP 400), nested calls can choose their own thread, and resumed runs preserve their original scopes (no adding new ones).

Trace Aggregation API (server + client + React)

Mastra adds trace aggregation end-to-end: POST /observability/traces/aggregate is discoverable via GET /observability/capabilities (traceAggregate), clients get mastraClient.aggregateTraces(), and React adds useTraceAggregateAvailable(). Field discovery now returns an aggregate schema describing valid group-by dimensions and measures.

Typed Signal Queueing for Agents (queueSignal)

You can now queue a typed signal behind the active run—without degrading it into a plain user message—via Agent.queueSignal() / session support and the new server route POST /agents/:agentId/queue-signal. Runs triggered by a signal also record signal metadata on the AGENT_RUN span for better traceability.

Faster, more usable trace lists with stored input/output/error previews

Trace and span list APIs now return outputPreview and errorPreview (and spans add errorTruncated), backed by a shared preview format in @mastra/core/storage (buildPreview, PREVIEW_MAX_LENGTH). Stores (ClickHouse/Postgres/DuckDB) persist previews at write time so queryTraces()/querySpans() no longer need to read full payloads—improving latency, memory use, and enabling UIs like Playground’s new Traces “Output” column.

Better reliability for long-running & durable workflows

Workflow execution is more robust: .foreach() failures now correctly fail the run (instead of hanging or corrupting output), durable agents are correctly honored inside workflow steps across engines, resume behavior preserves suspendData after crashes, and invalid resume data now throws AGENT_RESUME_INVALID_DATA without consuming the suspended run.

Breaking Changes

  • buildInputPreview() and INPUT_PREVIEW_MAX_LENGTH are deprecated in favor of buildPreview() and PREVIEW_MAX_LENGTH.
  • Factory sessions no longer have modes (Plan/Explore removed); thinking defaults and related config endpoints now use a single global default, and PUT /web/config/thinking rejects modeDefaults.
  • Factory credential behavior changed: cross-scope credential fallback is removed; custom credential providers must move from orgFirst to explicit credentialScope / policy resolution.

Changelog

@mastra/core@1.78.0

Minor Changes

  • Added scopes to agents: the addresses a run acts as, written as <type>:<value> (for example org:acme, resource:user-123, thread:conversation-123). (#26593)

    Setting scopes. Pass them per call on stream, generate, network, sendMessage, queueMessage and sendSignal, set them on the agent, or set them from trusted middleware with the new MASTRA_SCOPES_KEY request context key. Mastra combines all three. Read an agent's configured scopes with agent.getScopes().

    Memory. Memory reads its resource and thread from resource: and thread: scopes, in addition to memory.resource, memory.thread and the existing request context keys. If they name different values, the call throws a 400 error instead of silently picking one. Runs with no scopes from any source behave as before.

    const agent = new Agent({
      id: 'support',
      model: 'openai/gpt-5-mini',
      memory: new Memory(),
      scopes: ({ requestContext }) => {
        // Set by your auth middleware. Request bodies can fill unreserved keys, so only read trusted ones.
        const orgId = requestContext.get('org-id');
        return orgId ? [`org:${orgId}`] : [];
      },
    });
    
    // Before
    await agent.generate('Hi', { memory: { resource: 'user-123', thread: 'conversation-123' } });
    
    // After
    await agent.generate('Hi', { scopes: ['resource:user-123', 'thread:conversation-123'] });

    Nested calls. Tools, processors and subagents see the run's other scopes (for example org:acme) under MASTRA_SCOPES_KEY, but not its resource: and thread: scopes, so nested agent calls can choose their own thread.

    Suspended runs. Suspended runs keep their scopes when resumed: a resume may repeat some or all of them but cannot add new ones.

    Upgrading. Upgrade @mastra/server together with this release. While an older server is registered, agents refuse scopes from the call and the request context with an AGENT_SCOPES_SERVER_OUTDATED error (HTTP 400); the agent's own scopes option still applies. This detection relies on the server registering itself with Mastra, which @mastra/server has done since 1.0.0-beta.5. Servers older than that — and custom server adapters that never call mastra.setMastraServer() — are indistinguishable from no server at all, so they are not detected: upgrade them rather than relying on the guard.

    Runs suspended before this release have no recorded scope set; resuming one adopts the scopes supplied at resume time (the same trust checks as a fresh run apply) instead of rejecting them. Runs suspended after the upgrade record their set — including the empty set — and a resume may not add to it.

  • Added traceAggregateDiscoverySchema and an optional aggregate field on the trace-query fields response, describing the dimensions and measures aggregateTraces() accepts. (#27185)

  • Added queueSignal() to Agent and AgentController Session. It queues a typed signal behind the active run like queueMessage(), but keeps the signal type, tag, and attributes instead of turning it into a plain user message. (#27081)

    agent.queueSignal(
      { type: 'reactive', tagName: 'skill', attributes: { name: 'triage' }, contents: 'Triage the new issue.' },
      { resourceId: 'user-123', threadId: 'thread-abc' },
    );

    Runs started by a signal now record it on the AGENT_RUN span as attributes.signal (id, type, tagName, attributes), so traces show which signal started a run.

  • Memory can now contribute error processors to an agent. MastraMemory.getErrorProcessors() works like getInputProcessors() and getOutputProcessors(): the agent appends what it returns to its resolved errorProcessors, on both regular and durable agents. These processors run after configured and default error processors, and stay in place when a call passes its own errorProcessors. The base implementation returns none, so existing memory classes are unaffected. (#26029)

    class MyMemory extends MastraMemory {
      async getErrorProcessors() {
        return [new MyRecoveryProcessor()];
      }
    }
  • Added one preview format for span input, output and error. Trace lists (queryTraces(), listTracesLight()) now return outputPreview and errorPreview next to inputPreview, and querySpans() rows add errorPreview and errorTruncated. (#26567)

    New exports from @mastra/core/storage

    • buildPreview(value) builds a preview from a decoded payload, and buildPreviewFromJson(json) from stored JSON text. Both cut to PREVIEW_MAX_LENGTH (100) characters and end with … when cut.
    • Exported spans and span records accept optional inputPreview, outputPreview and errorPreview, preserved through create/update record builders. Stores trim nonblank supplied previews and cap them like generated ones (100 characters plus …, never splitting a surrogate pair); missing or blank previews are generated from the payload.
    import { buildPreview } from '@mastra/core/storage';
    
    buildPreview({ messages: [{ role: 'user', content: 'Where is my order?' }] }); // 'Where is my order?'
    buildPreview({ message: 'Tool failed' }); // 'Tool failed'

    Behavior changes

    • For a message list, previews now show the last user message instead of every user message joined with |.
    • querySpans() previews are now at most 100 characters of readable text plus an ellipsis instead of the first 256 characters of raw JSON.

    buildInputPreview() and INPUT_PREVIEW_MAX_LENGTH are deprecated. Use buildPreview() and PREVIEW_MAX_LENGTH. buildInputPreview(input, maxLength) still caps its preview at maxLength.

  • Added a beforeApproval hook to tools that require approval. It runs before the agent pauses for approval and can reject the call by returning { proceed: false, output }. The agent then skips the approval pause and does not execute the tool, and output goes back to the model as the tool result. Invalid calls are returned to the model to fix, and people are only asked to approve valid ones. (#26927)

    const issueRefund = createTool({
      id: 'issue-refund',
      inputSchema: z.object({ orderId: z.string(), amount: z.number() }),
      requireApproval: true,
      beforeApproval: async ({ orderId, amount }) => {
        const order = await orders.find(orderId);
        if (!order) return { proceed: false, output: { error: `Order ${orderId} not found` } };
        if (amount > order.paid) return { proceed: false, output: { error: `Amount exceeds ${order.paid}` } };
      },
      execute: async ({ orderId, amount }) => refunds.issue(orderId, amount),
    });

    The hook does not run again when the call resumes after an approval decision. If the hook throws, the error is logged and the call proceeds to the approval gate. Keep validating inside execute too, because state can change while the call is waiting for approval.

  • Removed the 31-day limit on trace search time ranges. You can now search all of your retained trace data, as long as the range starts before it ends. (#27042)

    // Before: rejected because the range is longer than 31 days
    // After: returns traces from the whole six-month range
    await mastraClient.queryTraces({
      timeRange: { from: '2026-04-01T00:00:00Z', to: '2026-10-01T00:00:00Z' },
    });

Patch Changes

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

  • Fixed resumeGenerate() and resumeStream() using up a suspended run when the resume data does not match the tool's resumeSchema. They now throw AGENT_RESUME_INVALID_DATA and the run stays suspended, so you can resume again with valid data. (#27130)

  • Fixed trace trees for agents that use output stream processors. In multi-step runs, each step's output stream processor span now sits inside that step and also covers the step's tool results, instead of every span showing up as a flat list under the agent run. Structured output processor spans stay under the agent run, because structured output runs once on the final result. (#27172)

  • Fixed live assistant messages missing the model that wrote them. Messages streamed by an AgentController session now carry modelId and provider in their metadata, so a client can show the model without reloading the thread. After a fallback, they name the fallback model. (#27030)

  • Fixed model inference spans so they record the messages sent to the model. Observability backends that read the inference span, such as Langfuse through the OpenTelemetry exporter or Datadog, now show the input for every model call instead of an empty value. (#26995)

  • Durable agent turns no longer create an unused tool stream writer on every LLM step. A writer is now created only when a step actually uses it. (#26987)

  • Fixed .foreach() on the evented workflow engine when an iteration fails. The run now ends with status: 'failed' and the iteration's error. Before, it hung forever (run.start() never resolved) or, with concurrency: 1, finished as success with a corrupted output array. Iterations that finish after the failure no longer mark the foreach step successful. (#26971)

  • Fixed workflow agent steps to support durable agents on both execution engines. (#26919)

  • Fixed full agent output repeating earlier text after a feedback continuation. (#26543)

  • Fixed a resumed workflow step losing its suspendData when the run restarts after a crash. The step now gets the payload it passed to suspend() again, alongside its resumeData. (#27127)

  • Disabled workspace tools no longer add setup work on every agent turn. Agents with workspace tools turned off now start each turn faster and allocate less memory. Fixes #26401. (#26657)

  • Fixed Amazon Bedrock India inference profiles (in. prefix, e.g. in.anthropic.claude-sonnet-5) sending temperature to models that reject it, which returned a 400. They now resolve model capabilities like the other geography prefixes. Fixes #26387. (#26982)

  • Fixed step scorers in nested workflows so their scores are saved. Before, scores from steps inside a child workflow were silently dropped. (#26984)

  • Fixed agent runs crashing with RangeError: Invalid string length when a command printed a very large amount of output. sandbox.executeCommand() now keeps the newest 1 MiB of stdout and stderr by default, the same limit spawned processes already use. Streaming callbacks still receive every chunk, and the result reports stdoutTruncated and stdoutDroppedBytes (and the stderr equivalents) when output was dropped. (#27106)

    If you need the complete output, pass maxRetainedBytes: Infinity:

    // Before: all output was kept by default
    const result = await sandbox.executeCommand('cat', ['data.json']);
    
    // After: pass Infinity to keep all output
    const result = await sandbox.executeCommand('cat', ['data.json'], { maxRetainedBytes: Infinity });

    Complete output is limited to 256 MiB per stream. A command that prints more is killed, and executeCommand() (or handle.wait()) rejects with ProcessOutputLimitError instead of crashing the process.

    The execute_command and get_process_output tools now tell the model when the start of the output was dropped, so it doesn't mistake the newest output for all of it.

  • Fixed document citations disappearing after a reload. When a model streams a document source (for example an Anthropic PDF citation), memory now stores it as a source-document part with its media type, title and filename. Previously it was saved as a URL source with an empty url, so reloaded conversations showed an empty link instead of the cited document. (#26805)

    const { messages } = await memory.recall({ threadId, resourceId });
    // assistant parts now include:
    // { type: 'source-document', sourceId: 'doc-1', mediaType: 'application/pdf', title: 'Q3 report', filename: 'q3.pdf' }

    AI SDK v5 UI messages now also keep source-document parts and the sourceId and title of URL sources when converting to and from stored messages.

  • Fixed durable recovery observers hanging when recovery fails before the stream starts. (#26918)

  • Fixed resumeGenerate() and resumeStream() saving messages to the thread when memory.options.readOnly is true. Resuming a suspended run now leaves read-only threads untouched, matching generate() and stream(). (#27056)

  • Fixed background task events leaking across resources. Previously, calling BackgroundTaskManager.stream() without a resourceId delivered every resource's task events, including tool arguments and results. Without a resourceId, stream() now only delivers tasks that have no resource. To subscribe to every resource, pass allResources: true: (#26895)

    const events = mastra.backgroundTaskManager.stream({ agentId: 'my-agent', allResources: true });

    The /background-tasks, /background-tasks/stream and /background-tasks/:backgroundTaskId routes now use the resource from the request context. A resource-scoped caller only sees its own tasks: requesting a different resourceId returns 403, and fetching another resource's task returns 404. An empty resource id in the request context is rejected with 403. Requests without a resource in the request context keep access to all resources.

    Also in: @mastra/server@1.78.0

  • Fixed agent controller channel sessions losing the sender's identity on the first message in a new chat thread. A channel session now opens directly on its chat thread, keeping the inbound request context, instead of creating an extra empty thread and switching over without it. Hosts that resolve memory per user from the request context's user no longer fail on a new thread. (#27113)

  • Fixed prompt caching after a suspended or tool-approval run resumes from Postgres storage. (#26943)

    Tool-call arguments and JSON tool results in model prompts now use sorted object keys, so the same content produces the same request after a run resumes. Stored messages are unchanged.

  • Fixed new conversation failures when ToolSearchProcessor uses context storage with memory. (#26902)

  • Fixed agents sending the same request until maxSteps when a text-only response ended with a non-standard finish reason, such as other. The run now ends after that response, so the text and token usage are no longer repeated. (#26905)

  • Fixed two OpenAI errors with tool inputs and structured output: (#26979)

    • Optional tool inputs that accept more than one type can now be left empty. Before, OpenAI models filled them with made-up values.
    • Structured output with a field that has free-form keys, such as z.record(z.string(), z.string()), now works on OpenAI instead of failing with a 400 error. These schemas are sent without strict mode. To keep strict mode anyway, set providerOptions.openai.strictJsonSchema: true.

    Also in: @mastra/schema-compat@1.3.15

  • Fixed createStep(agent) running a durable: true agent as a plain agent. When the agent is registered with Mastra, the workflow step now runs through the registered durable wrapper on both the default and evented engines, so durable streaming and cleanup apply as expected. (#27078)

    A step now logs a warning when a durable: true agent still has to run non-durably: when no durable wrapper for it is registered on the workflow's Mastra instance, or when it uses a legacy v1 model.

  • Fixed token estimates for long numbers. A long number used to count as one token, so output full of numbers could grow far past memory and token limits without triggering them. Long numbers are now counted accurately, and limits trigger at the right time. (#27208)

    Estimates saved on existing messages are recalculated, so existing conversations also get the corrected counts.

    Also in: @mastra/memory@1.38.0

  • Fixed tool transform replacing stored tool-call arguments with a Tool input-available payload unavailable placeholder when only another phase (such as transcript.output) was configured. A tool-level transform now only changes the phases it defines, so stored args round-trip unchanged and replaying the thread with Anthropic preserved thinking no longer fails. Transformers that throw or return undefined still fall back to the placeholder, and raw input deltas stay suppressed. Fixes #26597. (#26965)

@mastra/clickhouse@1.25.1

Patch Changes

  • Trace aggregates now read each trace's root span once from the requested time window, instead of re-reading every stored root row of the matching traces. This lowers aggregateTraces() latency, bytes read, and memory, most on large projects. (#26912)

  • Fixed ClickHouse deleteMessages bumping thread updatedAt with an ALTER TABLE UPDATE mutation, which rewrote whole data parts (often forbidden on managed deployments) and stamped the same updatedAt on every stored version of the thread row — making thread reads ambiguous on the ReplacingMergeTree until parts merged, so a just-renamed or archived thread could read back its previous state. The timestamp bump now inserts a strictly newer thread version, like every other thread write. Fixes #25580 (#26838)

  • Trace and span lists use less memory. Every span now stores short previews of its input, output and error when it is written, so queryTraces(), listTracesLight() and querySpans() read those previews instead of each span's full payloads. Trace and span lists now also return outputPreview and errorPreview. Spans written before this change still show previews, built from their own payloads, even when another tenant has a root span with the same trace and span IDs. (#26567)

    storage.init() adds inputPreview, outputPreview and errorPreview columns to mastra_span_events, mastra_trace_roots and mastra_trace_branches. This runs automatically on first use. If you set disableInit: true, run storage.init() from your migration step before deploying, or apply the columns yourself. Add them to mastra_trace_roots and mastra_trace_branches before mastra_span_events, because the views that copy span events into those tables select every column:

    ALTER TABLE mastra_trace_roots ADD COLUMN IF NOT EXISTS inputPreview Nullable(String);
    ALTER TABLE mastra_trace_roots ADD COLUMN IF NOT EXISTS outputPreview Nullable(String);
    ALTER TABLE mastra_trace_roots ADD COLUMN IF NOT EXISTS errorPreview Nullable(String);
    ALTER TABLE mastra_trace_branches ADD COLUMN IF NOT EXISTS inputPreview Nullable(String);
    ALTER TABLE mastra_trace_branches ADD COLUMN IF NOT EXISTS outputPreview Nullable(String);
    ALTER TABLE mastra_trace_branches ADD COLUMN IF NOT EXISTS errorPreview Nullable(String);
    ALTER TABLE mastra_span_events ADD COLUMN IF NOT EXISTS inputPreview Nullable(String);
    ALTER TABLE mastra_span_events ADD COLUMN IF NOT EXISTS outputPreview Nullable(String);
    ALTER TABLE mastra_span_events ADD COLUMN IF NOT EXISTS errorPreview Nullable(String);

    On a replicated cluster, add ON CLUSTER <cluster> after each table name.

    Nonblank supplied previews are trimmed and capped like generated ones (100 characters plus …). Missing or blank write-time previews are generated through the optional core helper. Stored empty strings do not trigger read fallback. With older core versions that lack the helper, preview fields pass through unchanged without generation.

  • Reduced the memory used by queryTraces() on large projects. Trace lists, numbered pages, delta polling and thread groups no longer sort every root span payload in the time range, so busy projects that previously hit ClickHouse memory limits now return results. Trace inputs and metadata are now read only for the traces on the returned page. (#26567)

    queryTraces() now treats each trace as having one root span. A trace that was written with more than one root span is listed once, under one of its roots, and matches root filters through any of them. Delta polling lists such a trace under the same root on every poll, and the next-page cursor still advances when a page comes back short.

@mastra/client-js@1.55.0

Minor Changes

  • Added aggregateTraces() to group and measure traces from the client. Check capabilities.traceAggregate from getObservabilityCapabilities() before calling it. (#27185)

    const { rows } = await mastraClient.aggregateTraces({
      timeRange: { from: '2026-08-01T00:00:00.000Z', to: '2026-08-08T00:00:00.000Z' },
      groupBy: ['entityName'],
      measures: ['count', 'duration.p95'],
    });
  • Added agent.queueSignal() for queuing a typed signal behind the active thread run. (#27081)

    await agent.queueSignal({
      signal: { type: 'reactive', tagName: 'skill', attributes: { name: 'triage' }, contents: 'Triage the new issue.' },
      resourceId: 'user-123',
      threadId: 'thread-abc',
    });

Patch Changes

  • Added an optional traceRetentionDays to the observability capabilities type and response schema, for hosts that serve GET /observability/capabilities themselves and know how long traces are kept. Clients can use it to stop users picking dates whose traces are already gone. Mastra's own server and built-in stores don't report a retention, so the field is omitted there. (#27166)

    const { capabilities } = await client.getObservabilityCapabilities();
    capabilities.traceRetentionDays; // e.g. 180, or undefined

    Also in: @mastra/server@1.78.0

  • Added outputPreview and errorPreview to trace list rows, and errorPreview and errorTruncated to querySpans() rows. (#26567)

    Also in: @mastra/react@1.11.0

@mastra/cloudflare@1.7.4

Patch Changes

  • storage.prune() now deletes durable agent run fencing records. Configure retention.workflows.runOwnership (by inactivity) and retention.memory.runFences (by time since retirement) to stop keeping one claim and one fence record per run forever. Fences of running executions are never pruned. (#26966)

    Also in: @mastra/cloudflare-d1@1.4.3, @mastra/dsql@1.5.3, @mastra/dynamodb@1.4.3, @mastra/mssql@1.10.4, @mastra/mysql@0.13.2, @mastra/oracledb@0.5.3, @mastra/spanner@1.9.5

@mastra/code-sdk@1.14.0

Minor Changes

  • createMastraCodeAgentController now accepts modes: false for hosts that don't offer modes. Every session runs as the single Build agent: a thread last used in another mode reopens in Build, and per-mode thinking defaults from settings never apply. Runs without a session override use the global thinking level. (#26854)

    const controller = await createMastraCodeAgentController({ modes: false });
  • You can now remove agent controller built-in tools such as submit_plan without reaching into the controller's private config. Set disableBuiltinTools on createMastraCode() / prepareAgentControllerMount(), or on MastraFactory to apply it to every Factory session. When submit_plan is unavailable, plan mode tells the model to write the plan file instead of calling it. (#26967)

    new MastraFactory({ storage, disableBuiltinTools: ['submit_plan'] });

    Also in: @mastra/factory@0.23.0

  • Removed cross-scope credential fallback in deployed sessions so each session uses only Factory or user credentials. (#26934)

    Custom credential store providers must replace orgFirst with an explicit credentialScope:

    // Before
    const tenant = { orgId, userId, orgFirst: true };
    
    // After
    const tenant = { orgId, userId, credentialScope: 'factory' as const };

    Use "user" to resolve only personal credentials. Hosts that select credentials for a session owner rather than the signed-in caller can register an asynchronous policy resolver as the second argument to setCredentialStoreProvider. Resolve that policy from authenticated identity and durable session records, not client-writable session state. Call prepareCredentialTenant(requestContext) before resolving a model; the SDK’s built-in model, memory, and goal-judge resolvers do this automatically and refresh the policy again before provider calls.

Patch Changes

  • Fixed revoked or invalid OAuth sign-ins showing a raw API error. When Anthropic, OpenAI Codex, GitHub Copilot, xAI, or Kimi For Coding rejects a stored sign-in with a 401, the error is now a ProviderAuthRequiredError that says the sign-in was rejected, the same as a missing sign-in. Hosts can match on the error name to prompt the user to sign in again. (#27187)

    if (error.name === 'ProviderAuthRequiredError') {
      // e.g. "Anthropic rejected the stored sign-in: OAuth access token is invalid."
      promptSignIn();
    }
  • Improved the "Tenant model credential policy could not be resolved" error. It now states why resolution failed: (#27138)

    • The request has no authenticated user.
    • No credential policy applies to the user.

    Added a warning with non-secret diagnostics and a stack trace. Use it to locate where the request context was lost.

  • Fixed model selection for Kimi For Coding, GitHub Copilot and custom providers in /models, /setup, /model and /om. Picking a Kimi or Copilot model no longer saves an id that fails with "Could not find config for provider mastracode". (#26692)

    These models are now listed under ids that resolve:

    • Kimi: kimi-code-plan-cn/<model> (your Kimi login or KIMI_API_KEY authenticates it)
    • GitHub Copilot: github-copilot/<model>
    • Custom providers: <custom-provider>/<model>

    Ids you already saved keep working: kimi-for-coding/<model>, mastracode/kimi-for-coding/<model> and mastracode/github-copilot/<model>.

    A Kimi API key saved in /api-keys or Factory's provider settings is now used for Kimi models, and a Kimi login shows as connected in /api-keys.

    Also in: @mastra/factory@0.23.0

  • Kept sandbox-backed file reads, listings and searches complete now that sandbox commands keep only the newest 1 MiB of output by default. (#27106)

  • Fixed the thinking level being ignored by more providers. It now reaches Groq, Mistral, xAI, OpenRouter, Together AI, DeepInfra, Cerebras, Perplexity and Alibaba models. (#26411)

    The thinking picker lists only the levels a model's request can carry. For example, GLM-5.2 through Mistral tops out at High instead of failing the request on Max. Models whose requests cannot carry a level now offer only Off instead of levels that did nothing. That includes Z.ai, Kimi, GitHub Copilot and Amazon Bedrock models. Off still leaves each model on its own default.

  • Fixed the missing provider credential error to say which credentials the Factory uses. It now names the Factory's model scope (organization or personal credentials only), notes that the other set doesn't apply, and says what to add. (#27077)

@mastra/convex@1.7.4

Patch Changes

  • storage.prune() now deletes durable agent run fencing records. Configure retention.workflows.runOwnership (by inactivity) and retention.memory.runFences (by time since retirement) to stop keeping one claim and one fence record per run forever. Fences of running executions are never pruned. (#26966)

    Redeploy your Convex functions after upgrading so the new prune operation is available.

@mastra/deployer@1.78.0

Patch Changes

  • Fixed mastra dev failing with "Missing subpath specifier" when the app depends on a private workspace package that has no version in its package.json. Fixes #26321. (#26991)

@mastra/deployer-cloudflare@1.2.37

Patch Changes

  • Fixed Cloudflare builds failing with Could not resolve "@opentelemetry/api" after posthog-node 5.56.1 started importing that optional package. When your project doesn't install @opentelemetry/api, the generated Wrangler config now points it at a stub, so wrangler dev and wrangler deploy work again. Fixes #27037. (#27079)

@mastra/deployer-sandbox@0.3.22

Patch Changes

  • Kept sandbox deploy commands and worker output reads complete now that sandbox commands keep only the newest 1 MiB of output by default. (#27106)

@mastra/duckdb@1.14.1

Patch Changes

  • Trace and span lists now read short stored previews instead of each span's full input. Every span event stores previews of its input, output and error when it is written, and queryTraces(), listTracesLight() and querySpans() read them. Lists also return outputPreview and errorPreview, and querySpans() adds errorPreview and errorTruncated. (#26567)

    Generated previews use the shared format from @mastra/core: at most 100 characters plus … when cut. For message lists they show the last user message. querySpans() previews used to be the first 256 characters of raw JSON. Nonblank supplied previews are trimmed and capped the same way; missing or blank write-time previews are generated from their payloads.

    Reconstructed previews stay aligned with the event carrying their payload, including newer empty or missing previews. Each event also records whether it carries an input, output or error payload, so listTracesLight() picks previews without reading the input and output payloads. NULL previews trigger batched payload reads and application-side generation; stored empty strings do not. With older core versions that lack the optional preview helper, preview fields pass through unchanged without generation.

    The store adds inputPreview, outputPreview, errorPreview, hasInputPayload, hasOutputPayload and hasErrorPayload columns to span_events when it starts. No manual migration is needed.

@mastra/e2b@0.14.1

Patch Changes

  • Changed the Factory sandbox memory setting from a free-form MB field to a dropdown of sizes: 512 MB, 1 GB, 2 GB, 4 GB or 8 GB, matching E2B's 8 GB limit. Memory set through the defaults option in code is unaffected. (#27098)

    Also in: @mastra/platform-workspace@1.8.1

@mastra/factory@0.23.0

Minor Changes

  • Added a Factory model scope setting so sessions can use organization models and credentials or their owner’s personal models and credentials. (#26934)

    Organization admins choose the scope through the project API:

    PATCH /web/factory/projects/:id
    Content-Type: application/json
    
    {"modelScope":"user"}

    Use "factory" for organization models and credentials. Credential scope changes apply on the next model invocation without replacing a thread’s selected model. Session ownership and project policy are resolved from durable records, and unavailable or ambiguous policy stops execution instead of falling back to another account.

  • Added a hybrid model scope: sessions run on the owner's credentials and default model, work items run on the Factory's. New Factories start in hybrid; existing Factories keep their scope. Because new Factories now bill members' interactive sessions to their own provider accounts, admins who want everything on organization credentials should switch the Factory to the factory scope. (#27095)

    Previously, setting modelScope to factory on PATCH /web/factory/projects/:id put both sessions and work items on organization credentials:

    { "modelScope": "factory" }

    Admins can now split sessions and work items by setting it to hybrid:

    { "modelScope": "hybrid" }
  • Factory sessions no longer have modes: Plan and Explore are gone, and every session runs as Build. Mastra Code in the terminal keeps its modes. (#26854)

    • The composer no longer shows a mode picker, and Shift+Tab no longer switches modes. /settings no longer prints a mode line.

    • A thread last used in Plan or Explore mode reopens in Build mode, so it can still write files.

    • Thinking defaults have a single global level. Per-mode thinking defaults no longer apply to Factory runs. Per-mode defaults saved by the terminal app stay in the shared settings file.

    • GET /web/config/thinking no longer returns modes or modeDefaults, and PUT /web/config/thinking only accepts globalDefault. A body with modeDefaults now gets a 400.

      - PUT /web/config/thinking  { "globalDefault": "medium", "modeDefaults": { "plan": "high" } }
      + PUT /web/config/thinking  { "globalDefault": "medium" }
    • Explore subagents in sessions without a default model now use Build mode's default model instead of Explore mode's, like sessions that have a default model already do for every subagent.

    • Audit events for runs no longer record an agent name.

  • Slack threads no longer create Work board cards. A Slack DM or @-mention still starts its session and posts the "New session started" card, whatever the factory's old setting was. (#27063)

    The slackWorkItemsEnabled project setting is removed from the project update API.

    Before:

    PATCH /web/factory/projects/:id
    { "slackWorkItemsEnabled": true }

    After: sent alone, the field is rejected as an empty update (400, "At least one project field is required"). Sent with other fields, it is ignored:

    PATCH /web/factory/projects/:id
    { "name": "Platform" }

    Slack-thread cards that already exist keep receiving aside comments and keep posting feed comments back to Slack.

Patch Changes

  • Added scope=user to GET /web/config/models to list models supported by the caller's personal credentials. Organization-only models are excluded from this personal catalog. (#27080)

  • Factory sessions and work-item runs can now start on a model you have credentials for. This applies when no default model was chosen and the built-in default model has no credential. (#27177)

    The session uses the default model of a provider you have connected, for example anthropic/claude-fable-5 for Claude or openai/gpt-5.6-sol for ChatGPT. A default you or your Factory chose always wins. The fallback is not saved as a default preference.

  • New Factories now default to the hybrid model scope everywhere. Onboarding preselects "Company work · personal sessions", and Factories with no saved scope, or one that isn't recognized, are treated as hybrid instead of company account. (#27202)

  • Improved the Slack "Could not authorize the owner of Slack Factory session" error. It now states which check failed: (#27183)

    • The sender's account link has no organization.
    • No source-control session row exists for the session, or its project repository row is missing.
    • The source-control session row has no organization.
  • The session detail route GET /web/user-sessions/:sessionId now returns the owner profile (owner: { id, name, avatarUrl? }), the same field the session list routes already return. owner is missing when the owner has no profile in the user directory. (#27031)

    const { session } = await fetch(`${baseUrl}/web/user-sessions/${sessionId}`).then(response => response.json());
    const isTeammateSession = session.userId !== viewerUserId;
    const ownerName = session.owner?.name ?? 'a teammate';
  • Fixed Factory pages outside a session failing with "Factory session ownership is unavailable." Overview, Rules, Settings and other Factory pages now use the Factory's model and credential policy for the signed-in member of that organization. (#27164)

  • Fixed new Factory sessions starting on the built-in mode model instead of your default model. When a session opened before its chat thread was attached, your default went to a thread the chat never used, so the first message could run on a model you have no credentials for and fail with a provider authentication error. (#27080)

  • Added scope=org to GET /web/config/models. It lists only the models your organization credentials can run, next to the existing scope=user for personal credentials. (#27163)

    GET /web/config/models?scope=org
    

    An unsupported scope value now returns 400 with invalid_model_scope. Before, it was ignored and the unfiltered list came back.

  • Fixed runs failing with "Factory source-control repository not found." after a repository's source-control connection was reinstalled. When a card's repository is linked but turned off in the Factory's environment, the error now names the repository and says to turn it on under Settings → Environment. (#26970)

  • Fixed model pickers offering models that an organization-scoped Factory cannot run. GET /web/config/models accepts an optional factoryId and then lists only models served by that Factory's model scope (organization credentials for organization scope, personal credentials for personal scope). Personal model settings now note when the current Factory uses organization credentials only. (#27077)

    GET /web/config/models?factoryId=<factory-id>
    
  • Fixed new chats so they run on the default model you saved in Settings. Before, the draft showed your default model but the chat ran on the built-in one. Chats where you already sent a message keep their model. (#26996)

  • Fixed sandbox settings becoming impossible to save when a previously stored value is no longer allowed (for example, a memory size above the new 8 GB limit). The outdated value is dropped and the provider default applies instead. (#27098)

  • Fixed the GitHub repository list failing entirely when an organization still had a personal-account GitHub installation it is no longer allowed to use; those installations are now skipped. Added a refresh option that bypasses the repository cache so repositories newly granted on GitHub appear without waiting for a webhook. (#27047)

  • Fixed Slack mentions failing with "Tenant model credential policy could not be resolved". New Slack sessions now open directly on their Slack thread and no longer leave an extra empty thread behind. (#27113)

  • Kept workspace file listings, change lists and untracked-file lookups complete now that sandbox commands keep only the newest 1 MiB of output by default. (#27106)

@mastra/inngest@1.10.6

Patch Changes

  • Inngest agents honor agent scopes when used with a @mastra/core release that supports them. Memory identity comes from resource: and thread: scopes, tools and processors see the other scopes, and a resumed run keeps the scopes it started with. Resuming with a scope the run did not hold throws an error and leaves the suspended run untouched. Scopes in the agent's defaultOptions count as the agent's own scopes, not the caller's. (#26593)

@mastra/libsql@1.26.2

Patch Changes

  • Fixed mastra_notifications being created without its ("threadId", "id") primary key. Without it, duplicate notification ids could be stored, and on PostgreSQL the table had no replica identity, so updates and deletes failed when the table was part of a logical replication publication. (#26664)

    New tables now get the key, and exportSchemas() includes it. Existing tables are upgraded the next time storage initializes: PostgreSQL adds the primary key, and LibSQL adds a unique index on the same columns. If the key cannot be added because an existing table already contains duplicate rows, or because the database role does not own the table, initialization still succeeds and logs a warning with the SQL needed to add the key.

    Also in: @mastra/pg@1.31.2

@mastra/loggers@1.3.8

Patch Changes

  • Fixed HttpTransport retrying JSON serialization failures. Log batches are now serialized once before sending: values that cannot be serialized (circular references, BigInts, throwing toJSON) fail immediately instead of waiting through retry delays, and network retries resend the exact same payload. (#27174)

  • Fixed FileTransport and UpstashTransport log queries accepting invalid page/perPage values. listLogs and listLogsByRunId now throw a RangeError for negative, fractional, non-finite, or unsafe pagination values instead of silently returning misleading results or sending malformed Redis ranges. page: 0 and perPage: 0 still default to page 1 and 100 per page. (#27175)

@mastra/mcp@2.4.1

Patch Changes

  • Fixed MCPClient sending malformed tool input schemas to model providers. One MCP tool with a broken schema (for example a required list placed inside properties) used to make strict providers such as OpenAI reject every request, even ones that didn't use that tool. (#25995)

    • Tools with a malformed input schema, or one over the nesting/size limit, are now skipped with a warning naming the server, tool, and problem. The server's other tools keep working.
    • toolsFromDefinitions skips such cached definitions; toolFromDefinition throws MCP_CLIENT_INVALID_TOOL_INPUT_SCHEMA for them.

    Fixes #23731.

  • MCP tools that declare an outputSchema now pass the text of embedded resource blocks to the model, together with their text blocks. Before, only the text blocks reached the model, so tools such as GitHub's get_file_contents showed the model a status line but not the file. structuredContent is still kept out of the model input. (#27131)

@mastra/memory@1.38.0

Minor Changes

  • Observational Memory now recovers automatically when a model provider rejects a request for exceeding its context window (#21657). (#25055)

    A provider can count more tokens than Observational Memory's own estimate (attachments, formatting, or switching to a model with a smaller window), so a request can overflow before the observation threshold is reached. Previously the agent's request just failed. Now Observational Memory observes everything still pending and retries once with the smaller context. No configuration is needed. Automatic recovery needs a @mastra/core version that includes MastraMemory.getErrorProcessors(); with an older core, the error reaches the caller as before. To turn it off:

    const memory = new Memory({
      options: {
        observationalMemory: {
          observation: { observeOnContextOverflow: false },
        },
      },
    });

    observe() also accepts force: true to observe pending messages regardless of the threshold, for that call only.

@mastra/nestjs@0.2.37

Patch Changes

  • The NestJS server marks itself as reserving agent scopes, like the other server adapters. Its routes already ignore scopes in request bodies and treat mastra__scopes as reserved, so agents accept scopes from your code and middleware when served through NestJS. (#26593)

@mastra/observability@1.20.1

Patch Changes

  • Update bundled model pricing with the latest prices from models.dev (bed1ccc)

  • Fixed cost estimates for Amazon Bedrock India inference profiles (in. prefix), which previously reported no_matching_model. Fixes #26387. (#26982)

  • Tracking exporters now wait for in-progress trace-cap evictions in flush() and shutdown(), so the aborted spans are sent instead of being queued after the flush or aborted twice. (#26916)

@mastra/otel-exporter@1.4.8

Patch Changes

  • Fixed tool calls going missing from gen_ai.output.messages. A model step that only calls tools is now exported as an assistant message with tool_call parts instead of an empty text message. Inference span inputs are also converted to the GenAI message format. (#26995)

@mastra/pg@1.31.2

Patch Changes

  • Trace and span lists now read short stored previews instead of each span's full input. Every span stores previews of its input, output and error when it is written, and queryTraces() and querySpans() read them. Lists also return outputPreview and errorPreview, and querySpans() adds errorPreview and errorTruncated. (#26567)

    Generated previews use the shared format from @mastra/core: at most 100 characters plus … when cut. For message lists they show the last user message. querySpans() previews used to be the first 256 characters of raw JSON. Nonblank supplied previews are trimmed and capped the same way; missing or blank write-time previews are generated from their payloads.

    listTracesLight() also reads stored previews. NULL previews trigger batched payload reads and application-side generation; stored empty strings do not. With older core versions that lack the optional preview helper, preview fields pass through unchanged without generation.

    storage.init() adds inputPreview, outputPreview and errorPreview columns to mastra_span_events. This runs automatically on first use. If you set disableInit: true, run storage.init() from your migration step before deploying, or apply the columns yourself (use your schema name if it isn't public):

    ALTER TABLE "public"."mastra_span_events" ADD COLUMN IF NOT EXISTS "inputPreview" text;
    ALTER TABLE "public"."mastra_span_events" ADD COLUMN IF NOT EXISTS "outputPreview" text;
    ALTER TABLE "public"."mastra_span_events" ADD COLUMN IF NOT EXISTS "errorPreview" text;

@mastra/playground-ui@64.0.0

Minor Changes

  • Added a minDate prop to DateTimeRangePicker and TraceTimeRangeChip, so you can stop users picking dates with no data, such as traces past retention. Days before minDate are disabled in both custom range calendars, and a range that already starts earlier can't be applied. (#27166)

    <TraceTimeRangeChip {...rangeProps} minDate={subDays(new Date(), retentionDays)} />
  • Comboboxes in forms now search right in the field. Inside a Field, a Combobox is a text field you type into to filter, and a multiple one shows its picks as removable chips (× or Backspace removes one) instead of "N selected". Comboboxes outside a Field (toolbars, breadcrumbs, inline pickers), ghost and icon-size triggers keep the trigger with the search in the popup. The Combobox props are unchanged. (#27112)

    Combobox is now built on new composable parts, also exported, for pickers its options prop can't describe: groups with labels, items with a title, description and end detail, or a custom field. They follow shadcn's Combobox structure; the root is ComboboxRoot because Combobox is the options-based component.

    import {
      ComboboxRoot,
      ComboboxInput,
      ComboboxContent,
      ComboboxEmpty,
      ComboboxList,
      ComboboxItem,
    } from '@mastra/playground-ui/components/Combobox';
    
    <ComboboxRoot items={agents} value={agent} onValueChange={setAgent} autoHighlight>
      <ComboboxInput placeholder="Pick an agent" />
      <ComboboxContent>
        <ComboboxEmpty>No agents found.</ComboboxEmpty>
        <ComboboxList>
          {item => (
            <ComboboxItem key={item.id} value={item}>
              {item.name}
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </ComboboxRoot>;

    For small triggers (an icon, a ghost button), use ComboboxTrigger with ComboboxSearch at the top of ComboboxContent to keep the search inside the popup.

  • DropdownMenu gains two typed helpers for one-of-many choices. DropdownMenu.RadioOptions renders a list of options, and DropdownMenu.RadioSub puts them in a submenu whose row shows the selected label, so a menu of several settings reads its current state without opening each submenu. onValueChange receives your option type, not any. DropdownMenu.SubTrigger also takes a hint to show a current value next to its chevron. (#27003)

    import { DropdownMenu } from '@mastra/playground-ui/components/DropdownMenu';
    import type { DropdownMenuRadioOption } from '@mastra/playground-ui/components/DropdownMenu';
    
    const ownerOptions: DropdownMenuRadioOption<'mine' | 'all'>[] = [
      { value: 'mine', label: 'Mine' },
      { value: 'all', label: 'Everyone' },
    ];
    
    <DropdownMenu>
      <DropdownMenu.Trigger>Properties</DropdownMenu.Trigger>
      <DropdownMenu.Content>
        <DropdownMenu.RadioSub label="Owner" options={ownerOptions} value={owner} onValueChange={setOwner} />
      </DropdownMenu.Content>
    </DropdownMenu>;
  • Added helpers to filter the metrics dashboard with FilterBar, the filter bar Traces uses. (#27034)

    • MetricsTimeRangeChip shows the dashboard's date range as the filter bar's Time chip.
    • createMetricsFilterBarFields and METRICS_FILTER_BAR_OPERATORS describe the dimensions you can filter on.
    • metricsTokensToFilterBarItems and filterBarItemsToMetricsTokens convert between FilterBar items and the dashboard's URL-backed filter tokens.

    DateRangeSelector and createMetricsPropertyFilterFields are deprecated in favor of these.

    import { FilterBar } from '@mastra/playground-ui/components/FilterBar';
    import type { FilterBarItem } from '@mastra/playground-ui/components/FilterBar';
    import { MetricsTimeRangeChip } from '@mastra/playground-ui/domains/metrics/components/metrics-time-range-chip';
    import {
      createMetricsFilterBarFields,
      filterBarItemsToMetricsTokens,
      METRICS_FILTER_BAR_OPERATORS,
      metricsTokensToFilterBarItems,
    } from '@mastra/playground-ui/domains/metrics/metrics-filters';
    import {
      TRACE_TIME_RANGE_FIELD,
      TRACE_TIME_RANGE_FIELD_ID,
      TRACE_TIME_RANGE_ITEM,
    } from '@mastra/playground-ui/domains/traces/components/trace-time-range-chip';
    
    const isTimeRange = (item: FilterBarItem) => item.fieldId === TRACE_TIME_RANGE_FIELD_ID;
    
    <FilterBar
      fields={[TRACE_TIME_RANGE_FIELD, ...createMetricsFilterBarFields(discovered)]}
      operators={METRICS_FILTER_BAR_OPERATORS}
      value={[TRACE_TIME_RANGE_ITEM, ...metricsTokensToFilterBarItems(filterTokens)]}
      onValueChange={items => setFilterTokens(filterBarItemsToMetricsTokens(items.filter(item => !isTimeRange(item))))}
    >
      <FilterBar.Chips
        renderChip={item => (isTimeRange(item) ? <MetricsTimeRangeChip /> : <FilterBar.Chip item={item} />)}
      />
      <FilterBar.Input />
    </FilterBar>;
  • Added createLocalStorageStore and useLocalStorageStore for a browser-stored value that every component reading it shares, along with other tabs. Unlike useLocalStorageState, a write updates every reader at once. A write that storage refuses still applies for the rest of the session. (#27025)

    import { createLocalStorageStore, useLocalStorageStore } from '@mastra/playground-ui/hooks/use-local-storage-store';
    
    const pinnedStore = createLocalStorageStore<ReadonlySet<string>>({
      key: 'app.pinned',
      defaultValue: new Set(),
      parse: stored => {
        if (!Array.isArray(stored)) throw new Error('not a list of ids');
        return new Set(stored.filter(id => typeof id === 'string'));
      },
      toJSON: pinned => [...pinned],
    });
    
    function PinButton({ threadId }: { threadId: string }) {
      const pinned = useLocalStorageStore(pinnedStore);
      return <button onClick={() => pinnedStore.write(new Set([...pinned, threadId]))}>Pin</button>;
    }

    Added Sidebar.NavShowMore, a "Show N more" / "Show less" toggle for a sidebar list cut to its first rows. It hides itself when nothing is cut off or when the sidebar is collapsed to its icon rail.

    <Sidebar.NavList>{(expanded ? threads : threads.slice(0, 5)).map(renderThread)}</Sidebar.NavList>
    <Sidebar.NavShowMore foldedCount={threads.length - 5} expanded={expanded} onExpandedChange={setExpanded} />
  • Added an inset variant to SettingsContainer for a short stack of rows that is the main content of a dialog. The default variant is unchanged and stays the one to use for settings pages and forms. (#27171)

  • Added an Output column to the Traces table, so you can see what a run returned without opening it. (#27195)

    • Shows the trace's output preview, or its error in red when the trace failed.
    • On by default, next to Input. Hide it from the Columns menu or through columnPreferences.
    • Saved column choices gain Output next to Input when Input is shown.
    • Servers that don't send previews show -.
    import { TracesListView } from '@mastra/playground-ui/domains/traces/components/traces-list-view';
    import { DEFAULT_TRACE_COLUMN_PREFERENCES } from '@mastra/playground-ui/domains/traces/trace-list-columns';
    
    <TracesListView
      {...props}
      columnPreferences={{
        ...DEFAULT_TRACE_COLUMN_PREFERENCES,
        visibleColumns: DEFAULT_TRACE_COLUMN_PREFERENCES.visibleColumns.filter(column => column !== 'output'),
      }}
    />;
  • Added THEME_OPTIONS, the System, Light, and Dark choices that ThemeToggle shows. Use it to offer the same theme choices inside a menu, where a segmented control does not fit. (#27097)

    import { DropdownMenu } from '@mastra/playground-ui/components/DropdownMenu';
    import { useTheme } from '@mastra/playground-ui/components/ThemeProvider';
    import { THEME_OPTIONS } from '@mastra/playground-ui/components/ThemeToggle';
    
    function ThemeMenuItem() {
      const { theme, setTheme } = useTheme();
      return <DropdownMenu.RadioSub label="Theme" options={THEME_OPTIONS} value={theme} onValueChange={setTheme} />;
    }
  • The trace thread view now shows the whole thread instead of only the last 31 days. Field suggestions on the traces page now use the full selected date range. (#27042)

    Removed clampTraceDiscoveryTimeRange and TRACE_QUERY_DISCOVERY_MAX_RANGE_MS from @mastra/playground-ui/domains/traces/trace-query-filters. Pass the selected time range directly:

    // Before
    const timeRange = clampTraceDiscoveryTimeRange(buildTraceQueryRequest(options).timeRange);
    
    // After
    const timeRange = buildTraceQueryRequest(options).timeRange;
  • Added Meter, a thin bar that shows usage against an allowance. Usage past the allowance turns red, sized to how far over it is. (#27194)

    import { Meter } from '@mastra/playground-ui/components/Meter';
    
    <Meter value={112} max={100} aria-label="vCPU" aria-valuetext="112 of 100 vCPU-hours" />;

    Pass Meter.Source children to show where the usage came from. Hovering or focusing the bar splits it into one color per source, with a key of each source's amount:

    <Meter value={148} max={360} aria-label="Memory">
      <Meter.Source label="research-workflows" value={112} />
      <Meter.Source label="Sandboxes" value={36} />
    </Meter>

    MetricsStackedBarChart can now print a label above a column. Set columnLabelKey to a row field, and rows that have it show the text above their column:

    <MetricsStackedBarChart data={rows} series={series} columnLabelKey="overage" />
    // rows: [..., { time: 'Oct 13', included: 100, over: 12, overage: '$1.20 over' }]

    An inset Table can now draw its header inside the inset with the rows, for a table nested in a card or an expanded row:

    <Table variant="inset">
      <Table.Header placement="inside">…</Table.Header>
      <Table.Body>…</Table.Body>
    </Table>
  • Added an info prop to FieldLabel. It shows a small info icon after the label with your text in a tooltip on hover or keyboard focus. Screen readers announce the text as the icon's description, and clicking the icon never focuses the field. (#27114)

    <FieldLabel info="Shown to people who pick this agent.">Display name</FieldLabel>

    Added InfoTooltip, the same info icon for use outside form labels, such as next to a heading or a toolbar control:

    import { InfoTooltip } from '@mastra/playground-ui/components/Tooltip';
    
    <InfoTooltip aria-label="Version information" side="bottom" align="start">
      Each save creates a version you can switch back to.
    </InfoTooltip>;

    TooltipContent takes keepMounted to keep a closed tooltip in the page (hidden), for example so its trigger can point aria-describedby at it.

    Removed RequestContextLabel (@mastra/playground-ui/domains/request-context/components/request-context-label). Use FieldLabel with info instead:

    <FieldLabel info="Values passed to the agent at runtime.">Request context</FieldLabel>

Patch Changes

  • Fixed avatars that stayed on the fallback initial after an image failed. They now show the new image when src changes. (#27097)

  • Fixed formatModelName for Bedrock model ids. Ids such as us.anthropic.claude-sonnet-4-5-20250929-v1:0 now read as "Claude Sonnet 4.5" instead of "Us.anthropic.claude Sonnet 4.5 20250929 V1:0". (#27001)

  • Fixed scrolling in long dropdown menus, context menus and selects. Their scrollbars now match the rest of the app and stay clear of the popup's rounded corners. (#27124)

  • Added an optional keywords field to Combobox options. Search matches these terms as well as the label, so an option can show a friendly label and still be found by an id or alias. (#27000)

    <Combobox
      options={[
        { label: 'Claude Haiku 4.5', value: 'anthropic/claude-haiku-4-5', keywords: ['anthropic/claude-haiku-4-5'] },
      ]}
    />
  • Added an optional renderDragPreview prop to ContentBlocks. When set, the dragged item is rendered on document.body, so a list inside a menu or popover no longer shows the dragged item far from the pointer. (#27028)

    <ContentBlocks items={groups} onChange={setGroups} renderDragPreview={group => <GroupRow group={group} />}>
      {groups.map((group, index) => (
        <ContentBlock key={group.id} draggableId={group.id} index={index}>
          {dragHandleProps => <GroupRow group={group} dragHandleProps={dragHandleProps} />}
        </ContentBlock>
      ))}
    </ContentBlocks>
  • Added an info tone to DataList rows. It adds a faint blue tint to set a row apart from the others, for example to mark the rows that match the current selection. Hover and selection still show through the tint. (#27055)

    <DataList.RowLink to={href} variant={isComparable ? 'info' : 'default'}>
      {cells}
    </DataList.RowLink>
  • Fixed the hover highlight in lists with collapsed sections. Rows in a collapsed section are now skipped, so the highlight keeps following the visible rows instead of staying at their old positions. (#27068)

  • Added describeSuggestionsError to FilterBar so apps can explain why value suggestions failed to load, for example when a server limit was hit. Return undefined to keep the default "Couldn't load values." text. (#27083)

    <FilterBar
      fields={fields}
      operators={operators}
      value={value}
      onValueChange={setValue}
      describeSuggestionsError={error => (isTooExpensive(error) ? 'Too many values. Narrow the time range.' : undefined)}
    >
      <FilterBar.Chips />
      <FilterBar.Input />
    </FilterBar>
  • Fixed the Expand button on clipped collapsible boxes looking see-through: it now uses the default solid button style, so it stays readable over the faded content. (#27084)

  • Fixed scrollbars in rounded popups, popovers, command lists and the composer so their ends no longer get cut off by the rounded corners. (#27124)

  • Changed PageLayout to scroll its body in a ScrollArea instead of reserving a stable scrollbar gutter. The body no longer shifts sideways when a scrollbar appears, and pages no longer show an empty strip beside their content. Added contentClassName to ScrollAreaViewport to style the wrapper around its children. (#27018)

    Added a mask prop to PageLayout to fade the body's scrolled edges, using the same values as ScrollArea. It is off by default. A fade covers whatever sits at that edge, so leave it off for bodies with sticky table headers.

    <PageLayout mask={{ y: true }}>{/* ... */}</PageLayout>
  • Fixed visual glitches when switching tabs in TabbedContainer. The search and filter controls now switch together with the table, so two search fields no longer overlap mid-switch and the panel corner no longer shows a gap or a stray control. (#26810)

  • Fixed code blocks updating state after they are removed from the page, which could throw window is not defined in tests. (#27104)

  • Fixed section labels in dropdown and context menus so they line up with the text of the items under them, including inset labels. (#27120)

  • Fixed Studio showing a resumed workflow run as Suspended. This happened when the run was resumed from code or the API, such as run.resume(). Studio asked for approval again. Studio now shows the run as running, with the approved step marked complete. (#26939)

  • Added a flush variant to InputGroupAddon. Use it to place adjacent number steppers at the field edge without a gap or edge inset. (#27017)

    <InputGroupAddon align="inline-end" variant="flush">
      <InputNumberDecrement />
      <InputNumberIncrement />
    </InputGroupAddon>
  • Fixed field labels catching clicks across the whole row. Clicking the empty space beside a text label no longer focuses its field; only the label text does. Labels that wrap a checkbox, switch or radio still toggle it from anywhere in the row. (#27110)

  • Studio traces: the error screen shows the server's reason instead of a generic message, and auto refresh stops retrying a request the server rejected so the page no longer flickers between the list and the error. DateTimeRangePicker also accepts an optional maxRangeMs prop that blocks custom ranges longer than the given duration. (#27076)

@mastra/react@1.11.0

Minor Changes

  • Added useTraceAggregateAvailable(), which reports whether the server supports trace aggregation. A server that does not report the capability is treated as not supporting it. (#27185)

Patch Changes

  • Review items returned by useReviewItems and useCompletedItems now include targetType, startedAt and completedAt from the experiment result, so a review item can be rendered exactly like the same result on its experiment page (agent answer as text, run duration). (#27084)

  • Fixed useChat leaving a pending user message in the chat after a send fails with thread signals enabled. The optimistic message is now removed when the server never accepted it, so retrying shows the message once instead of a stuck pending copy plus the retried one. (#26975)

@mastra/schema-compat@1.3.15

Patch Changes

  • Fixed Azure OpenAI GPT models filling optional tool inputs with made-up values. They now leave optional inputs empty, as they already do through @ai-sdk/openai. This applies when the Azure deployment name contains an OpenAI model name, such as gpt-4o, my-gpt5-deployment or o3-mini. Deployments with other names, such as production, and non-OpenAI models served through Azure behave as before. (#26680)

@mastra/server@1.78.0

Minor Changes

  • Added traceAggregate to GET /observability/capabilities, so clients can tell whether POST /observability/traces/aggregate is available. Trace-scope field discovery (POST /observability/traces/query/fields) now also returns an aggregate block listing the groupable dimensions and available measures when the store supports aggregation. (#27185)

  • Added POST /agents/:agentId/queue-signal, which queues a typed signal to run after the active thread run completes, or starts a run right away when the thread is idle. The body takes the same signal shape as POST /agents/:agentId/signals. (#27081)

    curl -X POST http://localhost:4111/api/agents/support-agent/queue-signal \
      -H 'Content-Type: application/json' \
      -d '{
        "resourceId": "user-123",
        "threadId": "thread-123",
        "signal": {
          "type": "reactive",
          "tagName": "skill",
          "attributes": { "name": "triage" },
          "contents": "Triage the new issue."
        }
      }'

Patch Changes

  • Agent scopes support: (#26593)

    • Agent routes ignore scopes in request bodies (including ifIdle.streamOptions.scopes), and mastra__scopes is a reserved request context key.

    • Set scopes from trusted server middleware instead:

      import { MASTRA_SCOPES_KEY } from '@mastra/core/request-context';
      
      server: {
        middleware: [async (c, next) => {
          c.get('requestContext').set(MASTRA_SCOPES_KEY, [`org:${orgIdFromAuth(c)}`]);
          await next();
        }],
      }
    • A conflicting resource: or thread: scope returns a 400 error.

    • With an older @mastra/server registered, agent calls that pass scopes — from the call or the request context — fail with AGENT_SCOPES_SERVER_OUTDATED (HTTP 400). Scopes configured on the agent itself still work. Upgrade the server to lift the restriction.

  • Tightened ownership checks on workflow run routes when server.auth.mapUserToResourceId is configured. A caller mapped to a resource can only act on its own runs, including when it passes an existing run id to /stream, /create-run, /start-async or /stream-legacy, and when it resumes, recovers or approves a durable agent run. The worker endpoints steps/execute and /workflows/events apply the same check to the run they address. Restarting all active runs of a workflow now requires a privileged caller. A caller without a resource id is treated as privileged only when a fine-grained authorization (FGA) provider is configured to authorize the route. (#26958)

    Before: with mapUserToResourceId configured, a caller with no mapped resource id could read and act on any run, and a mapped caller could act on runs saved without a resource id.

    After: a mapped caller acts only on its own runs. Admin, service, worker and push-broker callers that are not mapped to a resource need an FGA provider for run access, and restarting all active runs requires such a privileged caller. Runs saved without a resource id are no longer reachable by mapped callers.

    To keep privileged run access, configure an FGA provider next to the mapper:

    export const mastra = new Mastra({
      server: {
        auth: {
          authenticateToken: async token => verifyAccessToken(token),
          mapUserToResourceId: user => `${user.workspaceId}:${user.id}`,
        },
        fga: fgaProvider,
      },
    });

    Servers without mapUserToResourceId keep the previous rule: deny only when the caller's resource id and the run's resource id are both present and differ. That rule now also runs on routes that had no ownership check: /stream, /create-run, /start-async, /stream-legacy, steps/execute, /workflows/events, and the durable agent resume and recover routes. Reusing another resource's run id on those routes used to succeed and now returns 403.

@mastra/voice-sarvam@1.1.2

Patch Changes

  • Fixed SarvamVoice.listen() sending the speech model's API key. listen() now uses options.apiKey, then the listening model's key, and speak() uses the speech model's key, so projects with separate Sarvam keys for speech-to-text and text-to-speech bill each to the right key. (#27206)

    const voice = new SarvamVoice({
      speechModel: { apiKey: 'tts-key' },
      listeningModel: { apiKey: 'stt-key' },
    });
    await voice.listen(audio); // uses 'stt-key'
    await voice.listen(audio, { apiKey: 'per-call-key' }); // uses 'per-call-key'
  • SarvamVoice.listen() now throws on Sarvam API errors (e.g. invalid API key, rate limits) instead of silently returning undefined. Error messages for both listen() and speak() now surface Sarvam's error.message. (#27173)

    Before, a failed listen() call returned undefined. Now it throws, so handle it with try/catch:

    try {
      const text = await voice.listen(audio);
    } catch (error) {
      // error.message: "Sarvam AI API Error: <message>"
    }
  • Fixed SarvamVoice.listen() ignoring the listeningModel passed to the constructor. A voice created with listeningModel: { model: 'saaras:v3', languageCode: 'hi-IN', mode: 'translit' } now transcribes with that model, language and mode; before, it always sent saarika:v2.5 with automatic language detection. Options passed to listen() still take precedence. (#27102)

@mastra/weaviate@0.1.2

Patch Changes

  • Fixed $in filters with an empty array, which made Weaviate queries fail on text-array metadata. An empty $in now matches nothing and an empty $nin matches everything, like the other vector stores. (#26917)

Other updated packages

The following packages were updated with dependency changes only:

Don't miss a new mastra release

NewReleases is sending notifications on new releases.