github mastra-ai/mastra @mastra/core@1.79.0
October 21, 2026

6 hours ago

Highlights

MCP Events (poll/stream/webhooks + Studio inspection)

Experimental MCP Events (SEP-3415 draft) let MCP servers publish event streams that clients can poll, keep a stream open for, or receive via signed webhooks. Core/server/client add inspection APIs so Studio can list event types, stream occurrences, and test-emit events under mastra dev.

Observability Upgrades: Span Aggregation + “Time to First Chunk” + Richer Trace/Span Querying

New aggregateSpans() storage contract enables per-span analytics (e.g., p95 latency per tool, token/cost per agent) via the span-aggregate feature. Observability queries gain timeToFirstChunkMs fields, filtering, and sorting, plus expanded trace-scope fields (root entityId, root name, root spanType) and richer querySpans() context/parent/root-entity filters (with updated store capabilities to detect support).

Agent Controller Sessions Move to Session IDs (Multi-session per resource)

Sessions are now addressed by sessionId (not resourceId + scope) across core, server, and client-js, enabling many sessions per resource while keeping resourceId as the memory/thread owner key. New APIs include controller.getSession(sessionId), updated createSession()/deleteSession() signatures, and server routes under /agent-controller/:controllerId/sessions/:sessionId/....

Sandboxes Get a Native Filesystem API (Faster workspace operations)

WorkspaceSandbox can expose an optional filesystem for one-roundtrip reads/writes/listings instead of shelling out per operation. This lands across providers (e.g., E2B/PlatformSandbox) and is adopted by SandboxFilesystem in @mastra/code-sdk for faster file operations.

End-User Scoped Connect Integrations + Teams Channel

@mastra/connect adds per-resource (end-user) connection scoping for tools/credentials, preventing one user’s connections from serving another user’s requests, plus userConnectRoutes() to let authenticated end users connect their own accounts. Connect also adds Microsoft Teams as a supported channel.

Breaking Changes

  • Agent controller sessions are keyed by sessionId (scope removed): core/controller APIs, server routes, and client-js controller.session()/session.create() signatures updated.
  • Thread-row query contract changed: use timeRange.traces / timeRange.threads instead of traces.timeRange.
  • querySpans() contract changed: removed model/provider filters and dropped cost, inputTruncated, outputTruncated, errorTruncated from responses (requires updated storage adapters).
  • Channel resolveResourceId / resolveThreadId contexts now require requestContext (tests/manual calls must pass it).

Changelog

@mastra/core@1.79.0

Minor Changes

  • Breaking: Agent controller sessions are now addressed by their session id. Many sessions can share one resourceId, so resourceId only says whose threads and memory a session works on. (#27336)

    • Added controller.getSession(sessionId). Removed controller.getSessionByResource(resourceId, scope).
    • Removed the scope option from createSession(). Give each session its own id instead. When id is omitted it defaults to the resourceId, so existing one-session-per-resource code keeps working.
    • deleteSession() takes { sessionId }, and generateThreadTitle() takes an optional sessionId.
    • The controller request context exposes sessionId next to resourceId.
    // Before
    await controller.createSession({ resourceId: 'repo-1', scope: 'run-a' });
    const session = await controller.getSessionByResource('repo-1', 'run-a');
    await controller.deleteSession({ resourceId: 'repo-1', scope: 'run-a' });
    
    // After
    await controller.createSession({ id: 'run-a', resourceId: 'repo-1' });
    const session = await controller.getSession('run-a');
    await controller.deleteSession({ sessionId: 'run-a' });

    createSession throws when an explicit resourceId doesn't match the resource of the live session with that id, so a session id can't be used to take over another resource's session.

  • aggregateTraces() can now group by the root span's entityId, and trace queries and aggregates accept entityId as a trace-scope where field. Use it to split traces that have no entityName (for example, scorer runs recorded before scorers set one) by the primitive that produced them, then drill into a group by filtering on the same entityId. (#27684)

    const { rows } = await client.aggregateTraces({
      timeRange,
      where: { op: 'notExists', path: 'entityName' },
      groupBy: ['entityId'],
      measures: ['count'],
    });

    Trace-scope entityId reads the root span only. Inside spans.some and spans.none, entityId still matches any span in the trace. Stores that support the trace-scope field declare the new trace-query-root-entity-id storage feature.

    Also in: @mastra/clickhouse@1.26.0, @mastra/duckdb@1.15.0, @mastra/pg@1.32.0

  • Added the storage contract for span aggregation. Storage adapters can implement aggregateSpans() and advertise the span-aggregate feature to answer questions like "p95 latency per tool" or "token spend per calling agent" over individual spans. (#27669)

    class MyObservabilityStore extends ObservabilityStorage {
      getFeatures(): readonly ObservabilityStorageFeature[] {
        return [...(super.getFeatures() ?? []), 'span-aggregate'];
      }
    
      // `plan` is already validated: tool calls grouped by name, with count and p95 duration
      async aggregateSpans(plan: TrustedSpanAggregatePlan): Promise<SpanAggregateResponse> {
        // e.g. { rows: [{ dimensions: { name: 'search' }, measures: { count: 42, 'duration.p95': 812 } }], truncated: false }
      }
    }

    Span aggregates use the same request shape, measures, and limits as trace aggregates. Filters follow querySpans(), so counts match the spans querySpans() returns. Spans can be grouped by name, span type, and their own, parent, or root entity. Grouping by model or provider is not supported yet. Token and cost measures count usage on the span that recorded it. Adapters without the feature reject the call as not implemented.

  • Fixed DurableAgent.observe(runId) when attaching to a run from another process. (#26906)

    • Restored the agent's configured output processors, request context, and returnScorerData setting, including when the observing process also runs durable steps.
    • Preserved live call-time processors, request context, and scorer data when another wrapper observes the run within the same Mastra instance.
    • Behavior change: Cross-process DurableAgent.observe(runId) now throws DURABLE_AGENT_OBSERVE_AGENT_MISMATCH when the persisted run belongs to an agent with a different ID, matching resume() and recover(). Use the same agent ID in the observing process as in the process that started the run.
    • Made durable stream onChunk callbacks run serially and in order.
    • Made onFinish wait for pending onChunk callbacks to finish, without waiting for transport acknowledgements.

    Before: An observing process could use a different agent ID to attach to a run started by researcher:

    const observer = createDurableAgent({
      agent: new Agent({ ...agentConfig, id: 'researcher-observer' }),
      pubsub,
    });
    // After registering observer with Mastra and the shared storage:
    await observer.observe(runId);

    After: Use the starting agent's ID in the observing process. The previous configuration now throws DURABLE_AGENT_OBSERVE_AGENT_MISMATCH:

    const observer = createDurableAgent({
      agent: new Agent({ ...agentConfig, id: 'researcher' }),
      pubsub,
    });
    // After registering observer with Mastra and the shared storage:
    await observer.observe(runId);
  • Sandboxes can now expose their own disk through an optional filesystem property on WorkspaceSandbox. A provider with a file API answers reads, writes, and listings in one request each instead of a shell command per step. Paths are absolute inside the sandbox, and the provider decides containment. Fall back to executeCommand() when the property is absent: (#27859)

    const pkg = sandbox.filesystem
      ? await sandbox.filesystem.readFile('/workspace/app/package.json', { encoding: 'utf-8' })
      : (await sandbox.executeCommand?.('cat', ['/workspace/app/package.json']))?.stdout;
  • Added batch experiment ID filtering to observability metric queries, so you can get token usage and cost for many experiments in one request. (#27255)

    const result = await client.getMetricBreakdown({
      name: ['mastra_model_total_input_tokens', 'mastra_model_total_output_tokens'],
      aggregation: 'sum',
      groupBy: ['experimentId', 'name'],
      filters: { experimentIds: ['experiment-1', 'experiment-2'] },
    });

    Also in: @mastra/clickhouse@1.26.0, @mastra/client-js@1.56.0, @mastra/duckdb@1.15.0, @mastra/pg@1.32.0, @mastra/spanner@1.10.0

  • session.thread.transferOwnership now accepts an optional threadId, so you can transfer a thread without switching the session to it first. Without threadId it still transfers the active thread. (#27043)

    await session.thread.transferOwnership({ toOwnerId: 'user-456', threadId: 'thread-123' });
  • Channel resolveResourceId and resolveThreadId hooks now receive the per-message requestContext. Values a channel handler sets before calling defaultHandler, such as a tenant id, can be read directly in the resolvers instead of being looked up a second time. (#27436)

    channels: {
      handlers: {
        onDirectMessage: async (thread, message, defaultHandler, ctx) => {
          ctx.requestContext.set('tenantId', await lookupTenant(thread.channelId));
          await defaultHandler(thread, message);
        },
      },
      resolveResourceId: ({ requestContext }) => `tenant:${requestContext.get('tenantId')}`,
    }

    requestContext is a required field on ResolveResourceIdContext and ResolveThreadIdContext. Code that builds these objects by hand, such as tests that call a resolver directly, now needs to pass one:

    // Before
    await resolveResourceId({ platform, thread, message, defaultResourceId });
    
    // After
    await resolveResourceId({ platform, thread, message, defaultResourceId, requestContext: new RequestContext() });
  • Fixed workspace search and file listing ignoring .gitignore files below the workspace root. Content search and the file tree now apply every .gitignore relative to its own directory (for example, each repository in a multi-repo workspace) and skip node_modules unless the path points inside it. This keeps search from reading dependency files one by one on remote filesystems, and keeps file listings free of ignored and dependency files. (#27687)

    Filesystems that implement walk can now honor two new options: includeGitignoreFiles returns .gitignore files even when hidden entries are excluded, and skipDirectoryNames skips directories such as node_modules entirely.

    const entries = await filesystem.walk('.', {
      includeGitignoreFiles: true,
      skipDirectoryNames: ['node_modules'],
    });
  • Add opt-in bundler.pnpmWorkspaceSettings to write pnpm install-policy settings to generated deployment workspaces. Supported keys are allowBuilds, onlyBuiltDependencies, ignoredBuiltDependencies, minimumReleaseAge, minimumReleaseAgeExclude, trustPolicy, trustPolicyExclude, trustPolicyIgnoreAfter, and blockExoticSubdeps; explicit values override copied source settings and any other key is rejected before pnpm runs. Existing behavior is unchanged when omitted. (#27708)

    import { Mastra } from '@mastra/core/mastra';
    
    export const mastra = new Mastra({
      bundler: {
        pnpmWorkspaceSettings: {
          blockExoticSubdeps: false,
        },
      },
    });

    Also in: @mastra/deployer@1.79.0

  • Added iteration-aware final output processing so processors receive the current feedback-continuation iteration as its own assistant message and can transform each iteration independently. (#26346)

    Fixed feedback continuations with output processors so rewrites and redactions are preserved in getFullOutput().text and steps[].text instead of returning raw earlier-iteration text.

    Custom output processors that persist transcripts should use messageList.cloneForPersistence() to retain one assistant response with its original identity and order.

    // Before: may persist processor-visible response messages separately.
    const processor = {
      id: 'transcript-persister',
      processOutputResult: async ({ messages, messageList }) => {
        await saveMessages(messageList.get.response.db());
        return messages;
      },
    };
    
    // After: preserves one assistant response with its original identity and order.
    const processor = {
      id: 'transcript-persister',
      processOutputResult: async ({ messages, messageList }) => {
        await saveMessages(messageList.cloneForPersistence().get.response.db());
        return messages;
      },
    };
  • Added thread rows with activity timestamps, trace counts, previews, root-agent model details, filtering and sorting. (#27652)

    Breaking contract change: Thread-row requests use timeRange.traces or timeRange.threads instead of traces.timeRange. Related-trace predicates are not supported in the new contract. Legacy identity requests remain supported for mixed package versions.

    // Before
    const before = { traces: { timeRange: { from, to } } };
    // After
    const after = { timeRange: { traces: { from, to } } };
  • Added optional, experimental getEventsInfo() and getEventsInspector() members to MCPServerBase, plus the event info types (MCPServerEventInfo, MCPServerEventStreamFrame, MCPServerEventsInspector, …) and MCP_SERVER_EVENT_ERROR_IDS. Servers that declare MCP events use them to let Studio list, poll, stream and test-emit their event types. Servers without events don't need to implement them. (#27674)

    const info = server.getEventsInfo?.();
    console.log(info?.events.map(event => event.name));
    
    const inspector = server.getEventsInspector?.();
    const { events, cursor } = await inspector!.poll('ticket.created', { arguments: { project: 'web' } });
  • Added an embeddingDimension option to Memory. When you know your embedder's output dimension, setting it lets semantic recall skip the probe embedding call it otherwise makes once per Memory instance. Startup then makes no billed embedding call and doesn't fail when the embedding provider is unreachable. The vector index name stays the same, so existing indexes keep working. If the configured value doesn't match the length of the embeddings the embedder returns, Memory throws a MASTRA_MEMORY_EMBEDDING_DIMENSION_MISMATCH error naming both values before creating or querying any vector index. (#27588)

    const memory = new Memory({
      embedder: 'openai/text-embedding-3-small',
      embeddingDimension: 1536,
      options: { semanticRecall: true },
    });
  • Added timeToFirstChunkMs to trace and span queries. queryTraces() returns the root span's time to first chunk and querySpans() returns each span's, both null when no chunk was streamed. Both accept timeToFirstChunkMs filters with the same operators as durationMs. Stores that support it declare the trace-query-time-to-first-chunk feature. (#27277)

  • Support embedding specification v4 models from AI SDK v7. Core accepts v4 embedding models and exports embedV4; Memory and RAG use the matching SDK to generate embeddings while preserving v1, v2, and v3 behavior. (#27710)

    Also in: @mastra/memory@1.39.0, @mastra/rag@2.7.0

  • Added a datasetIds filter to dataset listing, so you can fetch specific datasets by ID in one request. All matches come back on one page, up to 100 IDs. (#27255)

    const { datasets } = await client.listDatasets({ datasetIds: ['dataset-1', 'dataset-2'] });

    Also in: @mastra/client-js@1.56.0, @mastra/libsql@1.27.0, @mastra/mongodb@1.24.0, @mastra/mysql@0.14.0, @mastra/pg@1.32.0, @mastra/server@1.79.0, @mastra/spanner@1.10.0

  • Added the root span's name and spanType as trace-scope fields for trace queries and trace aggregates. (#27321)

    • parseTraceQueryRequest() and parseTraceAggregateRequest() accept name and spanType in trace-scope where predicates.
    • planTraceAggregate() accepts them in groupBy and as countDistinct.name / countDistinct.spanType.
    • Trace-scope field discovery lists both with value suggestions, and the aggregate.dimensions list includes them.

    Inside spans.some and spans.none, name and spanType still match any span in the trace.

    const plan = planTraceAggregate(
      parseTraceAggregateRequest({
        timeRange: { from: '2026-08-01T00:00:00Z', to: '2026-08-08T00:00:00Z' },
        where: { op: 'eq', left: { path: 'spanType' }, right: { literal: 'workflow_run' } },
        groupBy: ['name'],
        measures: ['count', 'errorRate'],
      }),
    );

    Stores that support these fields declare the new trace-query-root-span-fields storage feature.

  • Agent schedules can now send an idle wake to the worker that has claimed the thread. Set ifIdle.requireClaimedOwner: true on the schedule, or return it from the prepare hook. This works the same way as agent.sendSignal: the claim holder runs the turn with its own claim stream options. If no owner answers, the fire fails with outcome: 'failed'. Before this change, the option was dropped, so whichever worker took the fire ran the turn itself. (#27896)

    await mastra.schedules.create({
      agentId: 'support',
      cron: '0 * * * *',
      prompt: 'Check in',
      threadId,
      resourceId,
      ifIdle: { behavior: 'wake', requireClaimedOwner: true },
    });
  • Added querySpans() filters and response fields for span context and parent/root entity information. For example: (#27272)

    where: { op: 'eq', left: { path: 'threadId' }, right: { literal: 'thread-123' } }

    Breaking: querySpans() no longer accepts model and provider filters or returns cost, inputTruncated, outputTruncated and errorTruncated. Model/provider values, name matching, duration filtering and previews remain available.

    The new filters require an updated storage adapter.

  • Trace and span queries can now sort by timeToFirstChunkMs, in either direction. Missing values sort last, and pages stay stable when values tie. Stores that support it declare the trace-query-time-to-first-chunk-sort feature. (#27473)

    const page = await storage.queryTraces(
      planTraceQuery(
        parseTraceQueryRequest({ timeRange, orderBy: [{ field: 'timeToFirstChunkMs', direction: 'desc' }] }),
      ),
    );
  • Span records now carry timeToFirstChunkMs, the milliseconds from span start to the first streamed chunk. The storage exporters fill it in, so storage adapters save the value without computing it. (#27140)

  • Added completionStartTime to agent run and model step span attributes, a getTimeToFirstChunkMs() helper that derives time to first chunk for any span, and carried the first-chunk clock through durable and evented agent runs so their generation and agent run spans report it too. (#26998)

  • Added backgroundTasks.untilIdleMaxIdleMs to set the default idle timeout for stream({ untilIdle: true }). Before, calls that did not pass maxIdleMs always closed after 5 minutes without new events. This included calls Mastra makes internally, such as waking an agent after sendMessage. A background task that ran longer than that finished without a continuation turn. A per-call untilIdle: { maxIdleMs } still takes precedence. (#27757)

    const mastra = new Mastra({
      backgroundTasks: {
        enabled: true,
        untilIdleMaxIdleMs: 30 * 60_000,
      },
    });
  • Added an opt-in apply_patch workspace tool that creates, updates, moves and deletes several files in one call. The tool checks the whole patch before it writes anything. If a write fails partway, it tries to undo the changes it already made. The tool is off by default. Enable it per tool: (#27913)

    new Workspace({
      filesystem: new LocalFilesystem({ basePath: './workspace' }),
      tools: { [WORKSPACE_TOOLS.FILESYSTEM.APPLY_PATCH]: { enabled: true } },
    });

    Setting the top-level tools.enabled does not turn it on, so existing agents keep using write_file and edit_file. Paths follow the same containment, read-before-write and write-lock rules as the other filesystem tools.

Patch Changes

  • Fixed agent runs that were aborted while a tool was running finishing with finishReason: 'tool-calls'. When a tool stops because the run's abort signal fired, the run now ends with finishReason: 'aborted', emits an abort chunk, and calls onAbort, the same as an abort during the model response. (#27748)

  • Fixed agent runs that kept calling the model and tools after losing their thread lease. When lease renewal fails, the run is now aborted instead of running beside the run that took over the thread on another process. Applies to regular, durable and evented agents. (#27581)

  • The error for a model with an unrecognized specificationVersion now lists v4 (AI SDK v7) among the supported versions. (#27516)

  • Fixed session messages starting while another agent runs on the same thread. When an AgentController uses an application's agent and has no pubsub of its own, its agents without their own pubsub now share the application's event bus. Session messages now wait for the other run to finish. A pubsub passed to the controller still takes precedence. (#27631)

  • Fixed two cases where an AgentController tool approval could get out of sync with its stored run: (#27371)

    • A failed resume no longer loses the answer. If approving or declining a tool call fails while the run is still waiting on it, the approval prompt is raised again (tool_approval_required) so the answer can be retried. Previously a retry returned { accepted: false, reason: 'not_pending' } and the run stayed suspended.
    • Detaching no longer declines a pending approval. session.thread.detachFromCurrent() now leaves a parked tool approval pending in storage, as documented, instead of declining it. It can be answered later, for example with respondToPersistedToolApproval.
  • Update provider registry and model documentation with latest models and providers (9b60f12)

  • Fixed agent turns stopping early when an awaited background tool returns no result. The agent now sends null to the model and continues. (#27482)

  • Fixed untilIdle continuations losing a completed background task's result when newer messages push it outside the configured history window. (#27205)

  • Fixed workspace_grep hanging for minutes on large workspaces. A directory search that runs on the host now has a 60-second budget. The budget covers filesystems without native grep, including the default LocalFilesystem, as well as the fallback used when a filesystem's native grep fails. Once the budget runs out or the agent run is aborted, the search stops starting new reads (reads already in progress finish) and returns the matches found so far. The summary line marks these results as incomplete and says how many files were scanned. The fallback log message now says why native grep failed (for example, a timeout) instead of always reporting an unsupported pattern. (#27464)

  • Reduced durable agent snapshot size: tool results are no longer stored twice per step, so long-running durable agents with large tool outputs write much smaller snapshots. stopWhen conditions still receive the full step content. (#27634)

  • Added bufferSize option to SystemPromptScrubber and ModerationProcessor. Set bufferSize: 200 to check per sentence instead of per chunk (fewer LLM calls, catches content split across chunks). Default unchanged; will become the default in a future major. (#27510)

    new SystemPromptScrubber({ model, bufferSize: 200 });
    new ModerationProcessor({ model, bufferSize: 200 });
  • Fixed live scorers passed at call time (agent.stream(..., { scorers })) or through defaultOptions.scorers never running unless the same scorer was also registered on the agent or on Mastra. Such scorers now run and save their scores on every agent engine, including durable and evented agents. (#27714)

  • Added byUser to the approved channel tool display event, matching denied. A custom toolDisplay function can now show who approved a tool call (for example "Approved by Alice"), not only who denied it. The default approved card also names the approving user. byUser is undefined in DMs. (#27584)

    toolDisplay: event => {
      if (event.kind === 'approved') {
        return { kind: 'post', message: `Approved by ${event.byUser ?? 'you'}` };
      }
    };
  • Channel streaming: text the agent writes before its first tool call now posts as its own message in toolDisplay: 'grouped' mode. An opening acknowledgement lands above the Thinking Steps widget instead of under it. (#27350)

    The plan widget in 'grouped' and 'timeline' modes is quieter. The header is a fixed "Working…" label and no longer echoes the current tool. Each task row shows the tool's description argument (first line only) when it has one, and falls back to toolName firstArg otherwise.

    Grouped mode is an adapter option on the channel provider:

    import { Mastra } from '@mastra/core';
    import { SlackProvider } from '@mastra/slack';
    
    export const mastra = new Mastra({
      channels: {
        slack: new SlackProvider({
          refreshToken: process.env.SLACK_APP_CONFIG_REFRESH_TOKEN,
          toolDisplay: 'grouped',
        }),
      },
    });
  • Fixed an agent run that had lost its thread lease running tools next to the run that took over the thread. Lease renewals run every third of the lease TTL, so a run whose lease was taken over could keep calling tools until its next renewal. A run that holds a thread lease now checks the lease owner before each tool call. If another run owns the lease, the run is aborted and the tool does not run. If the lease backend can't answer, the local lease deadline decides as before. (#27749)

  • Classifiers now accept refusal answers from decision models instead of throwing. Previously, when a decision model refused one question, decide() failed with Answer '<question>' does not match its question type. and any workflow .classifier() step failed the run. (#27488)

    A refused question now returns { type: 'refusal' } while the other questions keep their answers. RefusalAnswer is exported and included in every ClassifierAnswer type. Refusals have no choice, score, or probability, so equality checks such as answers.route.choice === 'billing' evaluate to false; check type === 'refusal' before relying on numeric fields.

    const { answers } = await classifier.decide({ state });
    
    if (answers.unsafe.type === 'refusal') {
      // Route to review, block, or apply a default.
    }

    Classifier scorers fail with an error naming the refused question, and ModelSelectionProcessor keeps the configured model and reports abstained: 'refused'.

  • Classifier evaluation traces now record the classifier as their entity, so they show the classifier name on the Traces page instead of "(not set)" and can be filtered and grouped like agents, workflows and scorers. EntityType gains a classifier value. (#27756)

    Also in: @mastra/client-js@1.56.0

  • Fixed Classifier.decide() crashing with state.map is not a function on decision providers built for @ai-sdk/provider 4.0.25 and newer, such as @ai-sdk/typesafe-ai 3.0.16+. String and JSON state are now converted to text and json parts before the provider is called, matching the AI SDK. Arrays are passed through only when every element is already a state part; other JSON arrays are wrapped in a json part. (#27298)

  • Fixed autoResumeSuspendedTools leaving a resumed plain tool marked as suspended. This happened when the model resumed a regular (non-agent, non-workflow) tool under a new tool call id. The original suspension was never cleared. Every later turn asked the model to resume the tool again. Reloaded clients also showed the call as still pending. The original suspension is now cleared, and a tool-call-resumed chunk is emitted for it. This applies to the standard, evented and durable agents. (#27784)

  • Fixed approval after a host disconnect without cancelling the parked agent run. (#27907)

  • Fixed workspace search and file listing so a deeper .gitignore can re-include what a parent ignores (for example !keep.log in a subdirectory overriding *.log), matching git. Search results from a filesystem's native grep now also apply .gitignore files in subdirectories of the search path. (#27695)

  • Classifier evaluation spans now show what the classifier was asked and what it decided. The span input holds the state and the questions, and the span output holds the answers, so routing decisions are visible in traces. Set hideInput and hideOutput in tracingOptions to keep them out of exported traces. (#27243)

  • Fixed durable and evented agents losing the conversation when a client tool is approved or declined on an agent without memory. The finish chunk now carries the same messages history as the default agent, so the follow-up model request keeps the original user prompt, the tool call and the tool result. The client tool also runs once: the first stream of a durable run stays open after suspension, and it no longer re-runs a tool the approve or decline request can execute. A server-side approve, which sends no client tools, still runs the tool on that open stream. (#27874)

    Also in: @mastra/client-js@1.56.0

  • Fixed durable agents not forwarding a declined sub-agent tool approval to the sub-agent. When a supervisor declines a tool approval raised by a sub-agent, the decline and its reason now reach the suspended sub-agent run, matching the approve path and the regular Agent. Declining the supervisor's own tool approval still denies the call locally. (#27374)

  • Fixed durable and evented agents placing isTaskComplete feedback after the model's next tool call and its result. The feedback now comes right before the model's retry, as it does with regular agents, so the model reads it as the reason for continuing. Fixes #27548. (#27743)

  • Fixed durable and evented agents ignoring per-model modelSettings in a model list. When an agent is configured with model: [{ model, modelSettings: { temperature: 0.1 } }, ...], each entry's settings now reach the provider on every attempt, including retries and fallbacks, instead of falling back to call-time settings. (#27727)

  • Fixed DurableAgent ignoring the reserved mastra__threadId and mastra__resourceId request-context keys. Durable and evented agents now route memory to the thread and resource set on the request context, as the regular Agent does, instead of to the thread or resource named in the call's memory options. (#27575)

  • Fixed durable agent resumes that lose a resume race or arrive after the run finished (#27365). (#27379)

    • A second approval of the same tool call fails right away with AGENT_RESUME_ALREADY_CLAIMED while the first resume runs in the same process, including when both approvals arrive at the same moment. Before, it waited for the whole resumed turn or about 5 s and then failed with DURABLE_AGENT_EXECUTION_CONFLICT.
    • A resume that loses the race reports the error only on its own stream. Clients following the run no longer get an error event or a span ended with an error while the winning resume completes.
    • Resuming a run whose snapshot was already deleted (for example because another instance finished it) now fails with AGENT_RESUME_NO_SNAPSHOT_FOUND and no longer writes a new pending workflow row.
    • A resume that loses the race no longer blocks the winning run's memory writes.
    • The durable execution lease now follows MASTRA_AGENT_THREAD_LEASE_TTL_MS (renewed every third of it). The default TTL drops from 30 s to 15 s, so a run left by a crashed process becomes recoverable sooner.
  • You can now resume durable agents (createDurableAgent, createInngestAgent) with the same resumeGenerate(resumeData, { runId }) call you use with Agent. (#27299)

    // Before (durable-only order, still supported but deprecated)
    await durableAgent.resumeGenerate(runId, { approved: true });
    
    // After (same as Agent)
    await durableAgent.resumeGenerate({ approved: true }, { runId });

    One deprecated call shape now throws instead of guessing: a string run ID followed by resume data that itself contains a string runId, with no third argument, for example resumeGenerate('run-A', { runId: 'x', approved: true }). To keep using the deprecated order, pass an explicit options argument: resumeGenerate('run-A', { runId: 'x', approved: true }, {}).

    Also in: @mastra/inngest@1.10.7

  • Fixed durable and evented agents dropping earlier tool calls from the prompt on a second output processor retry. When a processor called abort(reason, { retry: true }) more than once in a run that had already made tool calls, the next request lost the earlier tool call and its result, which broke the provider prompt cache. Durable and evented agents now keep the full history across retries, matching the standard agent loop (#27561). (#27758)

  • Fixed a durable AgentController session hanging after a prompt left unanswered for longer than MASTRA_SUSPENDED_RUN_TTL_MS. Later messages now run and finish as usual. The dropped prompt can still be answered. (#27632)

  • Fixed stopWhen on durable agents (DurableAgent, evented and Inngest) receiving steps without content. Predicates such as steps.at(-1)?.content.some(...) used to throw and end the run with a stream error. They now see the same step content that stream.steps reports. (#27524)

  • Fixed resumeGenerate() and getFullOutput() on durable agents sometimes never completing when a resumed run suspends again. (#27384)

  • Fixed durable and evented agents reporting no token usage for a step that suspends on a tool call. generate() results and tool-call-approval / tool-call-suspended chunks now include that step's usage, matching the regular agent. (#27493)

  • Fixed durable and evented agents ignoring generateTitle.emitEvent and onTitleGenerated. Streams from these agents now include the transient data-thread-title chunk before finish, and memory.onTitleGenerated is called with the saved title, matching the standard agent. (#27606)

  • Fixed durable and evented agents returning an empty response.messages when a processOutputResult processor rejects the final answer. response.messages now contains the assistant's draft, matching text, steps and the normal loop, so apps can still show the rejected reply to a reviewer. (#27737)

  • Fixed evented workflow restarts replaying completed steps and ensured recovered durable-agent streams finish without repeating saved effects. (#27852)

  • Fixed run.stream() on the evented workflow engine reporting workflow-finish with workflowStatus: 'success' when the run failed. The evented engine now emits the failed step's workflow-step-result before finishing, matching the default engine, so stream consumers and UIs see failed. (#27559)

  • Fixed evented .foreach() runs where more than one iteration fails. The run now keeps the first iteration's error instead of the last one, and fails exactly once. Previously, an iteration failing after the run had already failed replaced the stored error and published a second workflow.fail. Time-travel replays that hit a failure again also failed twice. (#26651)

  • Server model usage telemetry now counts tokens as model calls finish and reports them every 15 minutes and on shutdown as a new mastra_model_token_usage_v2 event. Each mastra_model_token_usage_v2 event contains only tokens the reporting process used, so redeploys, replicas, and lost home directories can't make it resend earlier usage. The startup mastra_model_token_usage event is still sent, unchanged, for compatibility, and can still resend earlier usage in those cases. Set MASTRA_TELEMETRY_DISABLED=1 to turn off telemetry. (#26986)

  • Fixed durable agent startup recovery so unrelated runs can recover concurrently. Timed-out attempts are checked again after settlement without starting duplicate executions or releasing live ownership. (#27768)

  • xAI models now accept xhigh reasoning effort. Groq, xAI, Together AI, DeepInfra, Cerebras and Alibaba models now run on the AI SDK v7 provider packages, like DeepSeek. (#26530)

    await agent.generate('Prove that there are infinitely many primes.', {
      providerOptions: { xai: { reasoningEffort: 'xhigh' } },
    });
  • Fixed fetchWithRetry leaving the bodies of failed HTTP responses open when it retried or gave up, which could keep connections busy until garbage collection. Failed responses are now cancelled before retrying or throwing; successful responses are still returned unread. (#27439)

  • Agent.reorderModels() and Agent.updateModelInModelList() now return true when the edit was applied and false when it was not (no model list, unknown id, or a function-valued model). Function-valued models are left untouched so per-request model selection keeps working. (#27192)

    if (!agent.updateModelInModelList({ id, enabled: false })) {
      // edit was not applied
    }
  • Fixed output stream processors dropping text they were still holding when a model stream ended. A reply shorter than one BatchPartsProcessor batch (for example with emitOnNonText: false) used to stream and save as empty; held parts are now released at the end of each model stream and passed through the processors that come after the one that held them. (#27565)

  • Fixed a Session that follows a thread keeping a parked tool call after another Session aborted the run, or after the resumed run ended with a non-success finish such as content-filter. The follower now ends the run and retracts the call with tool_suspension_cancelled, so suspensions.hasPending() returns false. This also works when the follower runs in a different Mastra instance that shares the same pubsub (#26997). (#27786)

  • Fixed a Session that follows a thread keeping a tool call marked as pending after the run was resumed on another instance and completed. The follower now drops the answered call, so suspensions.hasPending() returns false and aborting no longer emits a spurious tool_suspension_cancelled event. (#27226)

  • Fixed durable, evented, and Inngest agent streams to reject when stopWhen throws, matching plain agents. (#26921)

    Previously, these stream consumers could handle this failure as an error chunk. Catch the fullStream rejection instead:

    try {
      for await (const chunk of output.fullStream) {
        // Handle streamed chunks.
      }
    } catch (error) {
      // Handle the error thrown by stopWhen.
    }

    Durable transports reconstruct the error from its serialized name and message. The original stack, cause, and subclass identity do not survive a cross-process boundary. Core durable and evented workflows classify this failure with the reserved internal DURABLE_AGENT_STOP_WHEN_FAILED identifier. Inngest's serve transport uses the memoized step's stop-when- id prefix because its StepError serialization does not retain that identifier or its details. Any failure inside that Inngest step is therefore classified as a stopWhen failure.

    Also in: @mastra/inngest@1.10.7

  • Fixed Gemini 3 agents with working memory failing with "Function call is missing a thought_signature" when continuing a turn (for example after a client tool result). Gemini signs only the first tool call in a parallel step; when that call was updateWorkingMemory, removing it from saved history left the step without a signed first call. The next tool call in the step is now saved with Google's documented skip_thought_signature_validator signature, so the step replays on any Gemini client. (#27787)

    Also in: @mastra/memory@1.39.0

  • Fixed the goal judge reading the TrailingAssistantGuard's synthetic Continue. turn as the latest user message. On models that reject a trailing assistant turn (Claude 4.6+, Gemini 3+), the judge was told the user had said Continue. when they had not, hiding the real latest user message and miscounting assistant steps since it. The guard now marks its continuation, and the goal judge skips it. (#27503)

  • Simplified Azure gateway defaults and synchronous stream handling without changing runtime behavior. (#27646)

  • Fixed replies being saved as a separate assistant message on Gemini 3+ (and Anthropic 4.6+ with structured output) when the input ends on an assistant message. The reply is now saved into that assistant message, the same as with other providers. The synthetic user turn ("Continue." or "Generate the structured response.") Mastra sends to these providers is also no longer kept in the run's message list after the request is built. (#27503)

  • Fixed output stream processors that hold parts (such as BatchPartsProcessor and PIIDetector) re-emitting or leaking text when a model call failed. When a call is retried, the dropped attempt's text is no longer repeated before the retry's reply. When a call errors or is aborted, the text it was holding is dropped instead of being sent, and the error or abort still reaches the caller. Parts a processor holds back and sends again now reach the stream instead of getting stuck, so the reply's text-end is no longer lost. (#27615)

  • Fixed live DurableAgent observers attaching to runs owned by a different agent ID while preserving same-ID observation. Prepared runs now retain the DurableAgent's effective ID and name so ownership remains consistent after a restart. (#27353)

  • Stop an agent run as soon as another replica asks to abort it, even after the run has lost its thread lease. Previously the abort request was ignored once the lease was gone, so the run kept calling tools until its own lease renewal noticed the loss. (#27738)

  • Fixed token usage being counted twice after a tool call suspends and the run resumes. Spans and metrics for the resumed run now report only the tokens used after resuming, including when the resumed run suspends again. (#27634)

  • Fixed automatic durable agent recovery stopping after temporary storage or pubsub errors. Skipped runs are retried when the backend becomes available, without another restart or manual recovery. (#27354)

  • Fixed slow provider listing in mastra dev. Opening the model picker in Studio (GET /api/agents/providers) no longer re-reads the shared provider cache from disk for every provider, so the request drops from seconds to well under a second. (#26711)

  • Failed turns on the default Agent now save the same history as durable agents. (#27571)

    • A reply that fails mid-stream keeps the text streamed so far, followed by the error, instead of only the error.
    • When an input processor throws, the user message and the error are saved to memory, and the stream ends with an error chunk instead of the run failing with nothing saved.
  • Fixed live scorers passed to stream()/generate() via scorers, or set in an agent's defaultOptions.scorers, being skipped on the Inngest engine when they are not registered on the agent or on Mastra. They now run after the call, as they already do on the default and evented engines. (#27719)

    Also in: @mastra/inngest@1.10.7

  • Fixed tool calls disappearing from conversation history when a message or reminder arrived while the tool was still running. The call and its result now stay in history, so the model sees its own completed work on the next turn. (#26963)

  • Fixed a thread that stopped starting runs after a run waiting on a tool prompt was followed by another run on the same thread. Once that other run finished, every new message, signal or notification was reported delivered but never ran until the process restarted. The waiting run now gives the thread up when another run takes it over, and it can still be resumed with the user's answer. A run that is still waiting on its prompt keeps holding the thread as before. (#27545)

  • Fixed LocalSandbox commands that could hang past their timeout. When a command times out, it is asked to stop and then force-killed two seconds later if it is still running, including child processes that keep its output open. Previously a command that ignored the stop request kept the tool call waiting forever. (#26879)

  • Stop an agent run whose thread lease can no longer be confirmed. Previously, if lease renewals failed with an error or never answered (for example during a Redis outage on one process), the run kept calling the model and tools after its lease expired, alongside the successor run that took over the thread. Each run now aborts once one lease TTL has passed since its last confirmed acquire or renewal, the same way it already aborts when a renewal reports the lease lost. (#27731)

  • Fixed assistant message merging slowing down quadratically with the number of message parts. A step with thousands of reasoning items (common with OpenAI Responses reasoning) could previously block the event loop for tens of seconds; merging is now linear and produces the same merged messages. (#27912)

  • Fixed evented foreach restarts hanging when the next iteration had not started and nested workflow iterations re-running already completed child steps. (#26368)

  • Fixed MCP tool results containing images or audio being dropped by AI SDK v5 models and rejected by AI SDK v7 models such as Amazon Bedrock (unsupported tool content part type: image-data). MCP media content is now passed to every model in the format it supports. (#26662)

  • Fixed a mode's availableTools being dropped after the run paused for the user. Approving or declining a tool call, answering ask_user or request_access, or rejecting a plan used to continue the run with every tool the agent has, including tools the mode excludes. The continuation now gets the same restricted tool set as the first model call. (#27864)

  • Fixed notification dispatch stalling behind notifications it leaves pending. When a delivery policy holds notifications for another process's dispatcher (or they wait on an active run), dispatchDueNotifications now reads past them instead of re-reading the same first page every pass, so notifications behind them are still delivered. limit now counts only the notifications a pass delivers or fails. (#27426)

  • Fixed notifications that ran out of delivery attempts disappearing from the inbox. The notification-inbox tool's default list now includes failed notifications, reporting them as failed with their last delivery error, and marks them seen so they show up once rather than on every list. (#27453)

  • Fixed observe() hanging forever on a durable or evented agent run that was aborted while waiting for tool approval. Once the rejected approval or resume releases the run, observers now receive the abort and finish chunks and close. (#27923)

  • Fixed agent runs stopping when background observational memory buffering fails. The failure is still reported as an om_buffering_failed event, and the turn keeps going. A failed synchronous observation still follows the configured failure policy. (#27438)

  • Fixed a prompt waiting on the user disappearing from the session's display when another run on the same thread finished. The prompt could still be answered, but nothing showed it. Only the ending run's own prompts are now cleared. (#27555)

  • Fixed get_process_output with wait: true hanging after a background command had exited. When the command left a child running that still held its output open (cmd &, or a child of timeout), the wait never returned. It now returns once the command exits and its output has had a moment to drain, and says when a process the command started is still running. That process can still be stopped with kill_process, and is still stopped with the sandbox. A foreground command that starts a process in the background (cmd &) now returns once its shell exits rather than at its timeout, and the process it left behind is stopped when it returns. Use background: true for processes that should keep running. (#27578)

  • Fixed notifications that failed on their first delivery attempt. Delivery now retries automatically, up to five attempts, instead of staying pending forever. Retries preserve the claimed-owner requirement. (#25837)

  • Fixed Agent.stream() ending with a finish chunk (reason stop) when an output processor aborts in processOutputResult. The stream now ends with a tripwire chunk and status is 'tripwire', matching durable agents and other processor tripwires. (#27377)

  • Fixed resumed agent spans to remain children of the initial agent run across repeated suspensions, matching durable agent tracing. (#27149)

  • respondToPersistedToolApproval now accepts alwaysAllowCategory. Approving a tool call restored after a restart with "always allow" grants the tool's category to the run's thread, matching a live approval. (#27222)

    await session.respondToPersistedToolApproval({ toolCallId, approved: true, alwaysAllowCategory: true });
  • Fixed trajectory scorers in runEvals and dataset experiments getting a flat list of tool calls instead of the full trace when storage is configured. runEvals also no longer waits up to 6 seconds per item before scoring. (#27271)

  • Fixed a TypeScript error (TS2345) when chaining processor steps with .then() or .parallel(). Steps built with createStep(processor) now accept the output of a previous processor step, so the output guardrails workflow from the docs type-checks: (#27764)

    const workflow = createWorkflow({
      id: 'output-guardrails',
      inputSchema: ProcessorStepSchema,
      outputSchema: ProcessorStepSchema,
    })
      .then(createStep(new TokenLimiterProcessor({ limit: 1000 })))
      .then(createStep(new BatchPartsProcessor()))
      .commit();

    Also in: @mastra/inngest@1.10.7

  • Fixed approved tools added by an input processor running with the suspended run's request context. When you approve a tool call with approveToolCall({ requestContext }), a tool that an input processor added now sees the request context you passed, just like a tool defined on the agent. (#27570)

  • Fixed experiment comparisons when item, scorer, or experiment IDs match built-in object property names such as constructor, toString, or __proto__. Comparisons previously threw or dropped those entries; they now group and report them like any other ID. (#27718)

  • Fixed a misleading "No memory is configured but resourceId and threadId were passed in args" warning logged every time Observational Memory ran its Observer or Reflector. The warning now fires only when memory.thread and memory.resource are passed to an agent that has no memory, not when the ids come from the request context (auth middleware, AgentController), which every agent in the request inherits. (#27865)

  • Fixed several ways a suspended agent run could be resumed twice, never, or after it was aborted. (#27791)

    • Approving after abort: approving, declining, or resuming a run after abortRunStream() / abortThreadStream() hit its suspension now fails with AGENT_RESUME_ABORTED_RUN instead of running the tool or starting another model request. Fixes #27617.
    • listSuspendedRuns() after resume: DurableAgent now marks a resumed run as running as soon as it resumes, so it drops out of listSuspendedRuns() right away, including in other processes. A second resume of the same run fails with AGENT_RESUME_ALREADY_CLAIMED instead of running twice. Fixes #27619.
    • Resumer process dies: a running resume keeps renewing its claim. If the process dies, a later resume or approval fails with AGENT_RESUME_ALREADY_CLAIMED until the claim lapses, then takes the run over (or fails with AGENT_RESUME_RUN_FINISHED when recovery already completed it). Fixes #27366.

    Also in: @mastra/inngest@1.10.7

  • Fixed durable agent runs saving every tool output a second time in their final snapshot, which made the last snapshot write noticeably larger than the earlier ones. (#27672)

  • Fix experiment comparisons to average repeated trial scores per item instead of keeping only the last score. Items retain equal weight when experiments have different numbers of trials. (#27705)

  • Fixed notifications getting stuck when the delivery policy throws while the notification is being sent. The notification now records a failed delivery attempt and is retried by the notification dispatcher, up to the attempt limit, instead of staying pending forever. (#27736)

  • Fixed the 30-second default timeout for LocalSandbox foreground commands. A command run without a timeout could previously wait forever. Configured defaults and per-command timeouts are honored, including 0 to disable the timeout, and cloned sandboxes keep the configured default. Background processes keep their existing lifetime. (#26878)

  • Fixed respondToToolSuspension() hanging when answering a tool call (such as ask_user) that was parked during a run you aborted. The resumed run now streams to the Session and completes normally, instead of being silently dropped. (#26899)

  • Fixed system messages added by input processors (such as the TaskSignalProvider task-list instruction) going missing when an Agent resumes after tool approval. Fixes #24844. (#27372)

    Resumed runs now reuse the system prompt from the suspended run, matching durable agents. Instructions or system messages passed only at resume time no longer replace it.

  • Fixed Agent.resumeGenerate() and Agent.resumeStream() dropping returnScorerData on a plain Agent. A run started with returnScorerData: true now returns scoring data after a tool-approval or suspend resume, including when the resume happens in a different process or defaultOptions disable it. Passing returnScorerData to the resume call still overrides the run's value. (#27747)

  • Fixed lost messages when a per-step save fails. With savePerStep enabled, a failed write used to drop the drained messages from the unsaved set, so no later save wrote them and the user's message or the step's output disappeared from the thread. Failed saves now mark those messages unsaved again, and the next save writes them. (#27556)

  • Clarified that schedule onError and onAbort hooks only observe failures and aborts the schedule worker sees directly. Threaded schedules report failures after a run starts on the thread's own run, not through these hooks. (#27812)

  • Scorer run and scorer step spans now record the scorer name as entityName, falling back to the scorer ID. Previously scorer traces had no entity name, so filtering or grouping traces by entity name left them out. (#27684)

  • Fixed authenticated agent controller sessions failing with AGENT_MEMORY_THREAD_RESOURCE_MISMATCH when auth uses mapUserToResourceId. The controller now sets the reserved memory resource and thread keys to the session's own resource and thread. Other auth values in the request context are kept. (#27304)

  • Fixed channel conversations (Slack, Discord, Telegram and others) never getting an automatic title. New channel threads now start without a title, like any other thread, so generateTitle names them from the conversation. (#27251)

    Before, every new channel thread was saved with a generic label such as slack conversation, which counted as an existing title and blocked title generation. With generateTitle turned off, new channel threads now appear untitled in Studio, which lists them by creation date, instead of showing that label. Existing threads keep their current title.

  • Fixed trace query validation errors for an oversized in / notIn set pointing at the whole where predicate instead of its set. Zod 4.6.7 stopped reporting which union branch failed in this case. (#27699)

  • Bump for npm (#27221)

    Also in: @mastra/acp@0.4.3, @mastra/agent-browser@0.5.4, @mastra/agent-builder@1.1.30, @mastra/agentcore@0.5.2, @mastra/agentfs@0.2.2, @mastra/ai-sdk@1.11.2, @mastra/apple-container@0.5.1, @mastra/archil@0.3.1, @mastra/arize@1.3.26, @mastra/arthur@0.4.26, @mastra/astra@1.1.2, @mastra/auth@1.1.4, @mastra/auth-auth0@1.2.5, @mastra/auth-better-auth@1.1.8, @mastra/auth-clerk@1.3.1, @mastra/auth-cloud@1.2.7, @mastra/auth-firebase@1.1.4, @mastra/auth-google@0.1.6, @mastra/auth-neon@0.3.4, @mastra/auth-okta@0.2.5, @mastra/auth-studio@1.3.10, @mastra/auth-supabase@1.1.5, @mastra/auth-workos@1.6.7, @mastra/azure@0.3.2, @mastra/azure-ai-search@0.1.1, @mastra/blaxel@0.10.1, @mastra/braintrust@1.3.23, @mastra/brightdata@0.3.2, @mastra/browser-firecrawl@0.2.4, @mastra/browser-viewer@0.2.5, @mastra/chroma@1.1.4, @mastra/claude@0.3.3, @mastra/clickhouse@1.26.0, @mastra/client-js@1.56.0, @mastra/cloudflare@1.7.5, @mastra/cloudflare-d1@1.4.4, @mastra/cloudflare-sandbox@0.5.3, @mastra/code-sdk@1.15.0, @mastra/codemod@1.1.7, @mastra/connect@1.1.0, @mastra/convex@1.7.5, @mastra/couchbase@1.1.3, @mastra/cursor@0.3.3, @mastra/datadog@1.4.18, @mastra/daytona@0.11.3, @mastra/deepeval@0.1.20, @mastra/deployer@1.79.0, @mastra/deployer-cloud@1.79.0, @mastra/deployer-cloudflare@1.2.38, @mastra/deployer-netlify@1.2.38, @mastra/deployer-sandbox@0.3.23, @mastra/deployer-vercel@1.2.38, @mastra/discord@1.3.1, @mastra/docker@0.12.0, @mastra/dsql@1.5.4, @mastra/duckdb@1.15.0, @mastra/dynamodb@1.4.4, @mastra/e2b@0.15.0, @mastra/e2b-desktop@0.1.9, @mastra/editor@0.16.4, @mastra/elasticsearch@1.4.5, @mastra/elysia@0.1.19, @mastra/evals@1.10.8, @mastra/express@1.5.23, @mastra/factory@0.24.0, @mastra/fastembed@1.3.4, @mastra/fastify@1.5.23, @mastra/files-sdk@0.3.2, @mastra/gcs@0.3.4, @mastra/github-signals@0.5.2, @mastra/google-cloud-pubsub@1.1.4, @mastra/google-drive@0.2.3, @mastra/hono@1.7.21, @mastra/inngest@1.10.7, @mastra/isolated-vm@0.1.3, @mastra/koa@1.7.23, @mastra/laminar@1.3.28, @mastra/lance@1.5.1, @mastra/langfuse@1.6.2, @mastra/langsmith@1.3.28, @mastra/libsql@1.27.0, @mastra/livekit@0.5.1, @mastra/loggers@1.3.9, @mastra/longmemeval@1.1.38, @mastra/mcp@2.5.0, @mastra/mcp-docs-server@1.3.9, @mastra/mcp-registry-registry@1.1.6, @mastra/memory@1.39.0, @mastra/mesa@0.3.2, @mastra/modal@0.7.2, @mastra/mongodb@1.24.0, @mastra/mssql@1.10.5, @mastra/mysql@0.14.0, @mastra/nestjs@0.2.38, @mastra/next@0.2.37, @mastra/observability@1.21.0, @mastra/openai@1.1.3, @mastra/opencode@0.1.38, @mastra/opensearch@1.1.3, @mastra/oracledb@0.5.4, @mastra/otel-bridge@1.5.18, @mastra/otel-exporter@1.4.9, @mastra/parallel@0.1.3, @mastra/perplexity@0.2.2, @mastra/pg@1.32.0, @mastra/pinecone@1.1.2, @mastra/platform-workspace@1.9.0, @mastra/playground-ui@65.0.0, @mastra/posthog@1.4.3, @mastra/qdrant@1.1.5, @mastra/quickjs@0.1.4, @mastra/rag@2.7.0, @mastra/railway@0.10.1, @mastra/react@1.12.0, @mastra/redis@1.4.11, @mastra/redis-streams@0.6.4, @mastra/s3@0.6.4, @mastra/s3vectors@1.1.4, @mastra/schema-compat@1.3.16, @mastra/sentry@1.2.28, @mastra/server@1.79.0, @mastra/server-adapters-test-suite@0.1.3, @mastra/slack@1.7.3, @mastra/spanner@1.10.0, @mastra/sprites@0.1.1, @mastra/stagehand@0.3.6, @mastra/tanstack-start@0.2.37, @mastra/tavily@1.1.3, @mastra/teams@0.1.2, @mastra/telegram@0.2.3, @mastra/temporal@0.4.17, @mastra/turbopuffer@1.2.2, @mastra/turso@0.1.17, @mastra/upstash@1.5.4, @mastra/valkey@0.2.7, @mastra/valkey-streams@0.5.6, @mastra/vectorize@1.1.3, @mastra/vercel@1.6.1, @mastra/voice-aws-nova-sonic@0.2.5, @mastra/voice-azure@0.12.2, @mastra/voice-cloudflare@0.13.3, @mastra/voice-deepgram@0.13.2, @mastra/voice-elevenlabs@0.13.2, @mastra/voice-gladia@0.13.2, @mastra/voice-google@0.14.3, @mastra/voice-google-gemini-live@0.14.17, @mastra/voice-inworld@0.4.5, @mastra/voice-mistral@0.1.2, @mastra/voice-modelslab@0.2.2, @mastra/voice-murf@0.13.2, @mastra/voice-openai@0.13.3, @mastra/voice-openai-realtime@0.14.7, @mastra/voice-playai@0.13.2, @mastra/voice-sarvam@1.1.3, @mastra/voice-speechify@0.14.2, @mastra/voice-xai-realtime@0.2.16, @mastra/voyageai@0.4.2, @mastra/weaviate@0.1.3

  • Fixed System message must be at the beginning errors from models served through Chat Completions APIs (e.g. Qwen3.5/3.6 or Gemma 3 on vLLM). Multiple leading system messages are now merged into one for Chat Completions providers. Prompts are left unchanged when an earlier system message carries its own provider options (such as a cache breakpoint). Providers with a dedicated system field (Anthropic, Google, Bedrock) and the OpenAI Responses API are unchanged. (#27307)

  • Fixed skill tools (skill, skill_read, skill_search) pausing for approval when requireToolApproval is enabled globally. Previously every skill load in an approval-enabled agent (including durable agents) suspended for an approval nobody needed. (#27296)

  • Fixed stored skills ignoring the metadata filter. skills.list({ metadata }) and skills.listResolved({ metadata }) now return only skills whose resolved version metadata matches every key/value pair, with correct total and pagination. (#27462)

    const skills = await storage.getStore('skills');
    await skills.listResolved({ metadata: { publishedBy: 'user-123' } });

    Also in: @mastra/libsql@1.27.0, @mastra/pg@1.32.0

  • Fixed Slack channel messages so agents receive the text of Block Kit layout blocks (header, section, context, markdown), not just the notification fallback. Block-only messages now reach the agent. Thread history includes the block text too. (#27393)

  • Fixed input processor rejections being ignored by agents running in separate worker processes. Upgrade both API and worker processes to apply the fix. (#27730)

  • Fixed evented workflows losing recoverable snapshots when terminal notifications fail. (#27355)

  • Fixed durable agent startup recovery getting stuck behind a stalled run. Each run's recovery wait is limited to 30 seconds without cancelling its execution, and recovery stops starting new work during shutdown. (#27357)

  • Removed the gray-matter dependency. SKILL.md frontmatter is now parsed with the yaml package directly, which drops the vulnerable sprintf-js package (GHSA-hp3w-g68c-fv3c, no patched release available) from the production dependency tree. YAML frontmatter that worked before keeps working; JavaScript frontmatter (---js) is still rejected. (#27198)

  • Fixed durable agent runs losing user input when a worker stops before the run finishes. (#27835)

    Durable runs now stop before calling the model when the accepted input cannot be saved — for example when the storage write fails or the caller has no write permission on the thread — instead of answering without persisting it. Memory configured with readOnly still runs without writing.

  • Agent controller session routes no longer start a session as a side effect. Only POST /agent-controller/:controllerId/sessions starts one. Before, a message, model switch or state write that reached the server first created the session on a new random thread. The write then landed on a thread the client never read, so it looked lost. (#27474)

    Every other session route now acts on a started session. After a server restart it rebuilds the session from its stored thread. If the session was never started, the route returns 404 and creates nothing.

    const session = client.getAgentController('code').session(resourceId);
    await session.create({ threadId }); // start first
    await session.sendMessage('Hello'); // 404 if the session was never started

    AgentController.createSession accepts resumeOnly: true to get this behavior. It throws SessionNotStartedError when no stored thread matches. Factory now starts the session on its own thread when the session is created, so the first prompt can be sent right away.

    Also in: @mastra/factory@0.24.0, @mastra/server@1.79.0

  • Fixed paused tool calls picking up a different mode's tools when they resume. Approving, declining, or answering a paused tool call now uses the tools of the mode the run was paused in. Before, switching modes while a call waited gave the continuation the new mode's tools. A switch to an unrestricted mode could expose every tool to a run that started in a restricted one. The same applied when answering a suspension from another thread. Approving a plan still moves the run to the plan's transition mode. Fixes #27873. (#27911)

  • Fixed stopWhen receiving the wrong step content when savePerStep is enabled. After a run was resumed (for example after a tool suspended to ask the user a question), a step could show up with no tool results, so conditions such as "stop after N failed tool calls" counted wrong and fired late. (#27521)

  • Calling abort() with no reason from an output processor's processOutputStream now reports Tripwire triggered by <processorId> on durable and evented agents, the same as Agent.stream() and every other processor hook. Before this fix those engines reported Stream part blocked by <processorId>. (#27567)

  • Fixed tool payload transforms not being applied to streamed tool calls. When a model streamed tool-call arguments, the tool-call chunk sent to the stream had no transform metadata, so UIs showed the raw arguments instead of the redacted display version. The stream now emits the model's final tool call, which carries the transform. (#27501)

    A tool call ID of __proto__ no longer breaks the stream.

    A later step that reuses a tool call ID is now kept as its own call in toolCalls instead of being dropped.

  • Fixed missing token usage when a tool call suspends or waits for approval. Suspended stream() and generate() results now include the step's tokens. After resuming, totalUsage stays cumulative and token metrics count those tokens once. See #27232. (#27425)

  • Fixed messages being lost when sent to a thread shortly after the same instance cleaned up the thread's abandoned suspended run. Such a send used to return deliver while nothing received the message; it now returns blocked. Retry the send, and once the old run's lease expires (15 seconds by default) it starts a new run. (#27389)

  • Added AgentController.setThreadModel to change a session thread's model without recreating the session. A running session bound to the thread switches immediately; otherwise the model is saved for its next run. (#27591)

    const { live } = await controller.setThreadModel({
      resourceId: 'session-1',
      threadId: 'thread-1',
      modelId: 'openai/gpt-5.6',
    });
  • Fixed isBadRequestError retrying invalid API key errors. Newer OpenAI provider versions report invalid_api_key stream errors with status 400, so they were retried as bad requests instead of failing right away. Authorization errors now stay terminal. (#27788)

  • Fixed trace, span and thread query validation errors for oversized membership sets. An in/notIn predicate with more than 100 values points at the set again (for example where.set or where.spans.some.set). Since the zod 4.6.7 upgrade, the error pointed only at where. (#27782)

  • Improved cancellation of durable and evented agent tool and model calls after execution ownership is lost. Cooperative tools receive an abort signal so superseded work can stop sooner without canceling the recovering run. Recovery may run a tool again, so tools must be safe to repeat. Cancellation cannot undo work already completed. (#27490)

    Pass the tool's abort signal to operations that support cancellation:

    import { createTool } from '@mastra/core/tools';
    import { z } from 'zod';
    
    const fetchText = createTool({
      id: 'fetch-text',
      description: 'Fetch text from a URL.',
      inputSchema: z.object({ url: z.string().url() }),
      outputSchema: z.string(),
      execute: async ({ url }, context) => {
        // Without the signal, the request keeps running after ownership is lost.
        const response = await fetch(url, { signal: context?.abortSignal });
        return response.text();
      },
    });
  • Fixed timeTravel() running the same steps twice when called concurrently on one run. timeTravel() now atomically claims the run before executing, like resume() does. A losing call throws WORKFLOW_TIME_TRAVEL_ALREADY_CLAIMED (or WORKFLOW_TIME_TRAVEL_RUN_RUNNING if the run is already executing) and POST /api/workflows/:workflowId/time-travel-async returns 409 Conflict. The claim isn't enforced with storage adapters that don't support concurrent updates, when shouldPersistSnapshot excludes the running status, or in the Inngest runner; concurrent calls there can still run steps twice. Fixes #27279. (#27313)

    Also in: @mastra/server@1.79.0

  • Fixed agents crashing with Class constructor ... cannot be invoked without 'new' when a tool's schema is a class-based Standard Schema, such as an Effect Schema.Class adapted with Schema.toStandardSchemaV1. Tools can now use class-based schemas the same way as Schema.Struct. (#27317)

  • Fixed agents stopping after a server tool whose execute returns undefined. The tool now records a null result, and the agent includes the tool call and result in the next turn. (#27392)

  • Fixed trace scoring so type: 'agent' scorers receive the tool calls an agent made. Scoring a stored agent trace (for example with scoreTraceBatch or POST /api/observability/traces/score) missed tool calls recorded under a model step. Tool calls and MCP tool calls are now included in the order they started. Calls made inside sub-agents or inside a tool are not credited to the parent agent. (#27390)

  • Fixed UnixSocketPubSub intermittently failing to start a broker when the socket path is a bare relative name like events.sock or ./events.sock. The broker's random private socket name could look like a number (for example .28381e422682), which Node treated as a TCP port and rejected subscribe() with ERR_SOCKET_BAD_PORT or ENOENT. The private socket path now always keeps its ./ prefix. (#27894)

  • Fixed durable and evented agents losing a suspended run when an untilIdle stream is cancelled. Breaking out of streamUntilIdle() (or the request disconnecting) while the run waits for a tool approval or a suspend() resume now only stops reading, so resumeStreamUntilIdle() and approveToolCall() work afterwards instead of failing with "No registry entry found for run … Cannot resume". Cancelling while a tool is still running still aborts the run, and result.abort() still releases a suspended run. (#27563)

  • Tools that require approval now validate their input before asking for approval. Previously an agent asked a person to approve a call whose arguments did not match the tool's inputSchema, and the call then failed validation after it was approved. Now the validation error goes straight back to the model so it can retry, and no tool-call-approval is emitted. This applies to Agent, durable agents, and agent networks. Fixes #26771. (#26973)

  • Security: bump the internal @ai-sdk/provider-utils-v6 alias from 4.0.40 to 4.0.56 to pick up the fix for AIKIDO-2026-526145. No public API changes. (#27023)

  • Fixed deferred notifications running with no request context when the agent's delivery policy throws at delivery time. The delivery attempt now fails with the policy's error and is retried up to the attempt limit, instead of falling back to a bare wake that runs without a user. (#27729)

    Fixed low-priority notification summaries failing to save when the agent's memory depends on who owns the thread. The summary is still saved without starting a run, but now carries the delivery policy's request context.

  • Fixed observation of unknown and completed durable agent runs so streams close promptly after replaying retained events, including when the requested offset skips the final event or a completed workflow snapshot remains after cleanup. Observers stay attached when lifecycle state cannot be read. (#27358)

  • Fixed a message sent from an AgentController session being dropped when another agent was mid-run on the session's thread. The message now waits for that run to end and then runs. Pressing Stop while it waits drops it. If that run is waiting for an answer to its own prompt, the message isn't sent and the session shows an error saying so. A message sent while the thread is still held by a waiting run that was dropped from memory now waits for that hold to expire, then runs. Signals sent straight to the agent still report the thread as blocked. (#27624)

  • Fixed goal judges and subagents hanging inside an Agent Controller session. Previously they inherited the session's memory thread and waited on it forever. Goal judges now use their own goal-specific thread. Forked subagents use the cloned thread. Non-forked subagents no longer inherit the session's thread and resource IDs. (#27323)

  • Boot-time workflow recovery no longer re-runs a workflow run that another live server instance is still executing. Previously, when one replica started while another was mid-run, restartAllActiveWorkflowRuns() restarted every running run and its steps executed twice. (#27508)

    Workflow runs now hold the existing storage run-ownership lease (the one durable agents use) while they execute. run.restart() throws a WORKFLOW_RESTART_RUN_OWNED error instead of taking over a live run. The boot sweep skips those runs and checks them again once the lease could have expired, so runs left behind by a crashed process are still recovered. Storage adapters without run-ownership support keep the previous behavior.

@mastra/acp@0.4.3

Patch Changes

  • Fixed ACP prompts hanging when the agent stops with max_tokens, cancelled, max_turn_requests or refusal. prompt(), AcpAgent.generate() and ACP tools now reject with ACP prompt stopped before completing: <reason>. AcpAgent.stream() still returns a result, but its text promise rejects and reading fullStream errors with the same message. Reading only fullStream no longer leaves an unhandled rejection. (#27927)

@mastra/astra@1.1.2

Patch Changes

  • Simplified vector inclusion options while preserving boolean request values. (#27646)

@mastra/clickhouse@1.26.0

Minor Changes

  • Added timeToFirstChunkMs to ClickHouse trace and span queries. queryTraces() returns the root span's time to first chunk and querySpans() returns each span's, both null when no chunk was streamed. You can also filter on it: (#27277)

    where: { op: 'gt', left: { path: 'timeToFirstChunkMs' }, right: { literal: 1000 } }
  • ClickHouse trace and span queries can now sort by timeToFirstChunkMs, slowest or fastest first. Traces and spans without a value come last. (#27473)

    orderBy: [{ field: 'timeToFirstChunkMs', direction: 'desc' }];
  • Added thread rows with previews, trace counts, activity timestamps, filters and cursor pagination. Legacy thread identity queries remain supported. (#27652)

    Broad thread queries exceeding the retained-history limit return a resource-limit error. Narrower time ranges can still succeed.

    import { planThreadRowsQuery } from '@mastra/core/storage';
    
    await storage.queryThreads(
      planThreadRowsQuery({
        timeRange: { traces: { from, to } },
      }),
    );
  • Spans now store their time to first chunk in a new timeToFirstChunkMs column, ready for filtering and sorting by how fast a model started responding. Existing databases get the column automatically when the store starts. It stays empty for spans written before the upgrade or with an older @mastra/core. (#27140)

    Also in: @mastra/duckdb@1.15.0, @mastra/pg@1.32.0

  • Added querySpans() support for context and parent/root entity fields, including thread filtering: (#27272)

    where: { op: 'eq', left: { path: 'threadId' }, right: { literal: 'thread-123' } }

    Breaking with the updated core contract: querySpans() no longer accepts model and provider filters or returns cost, inputTruncated, outputTruncated and errorTruncated.

    Reduced memory usage when querying spans and displaying previews for older data.

Patch Changes

  • Fixed ClickHouse trace queries counting or grouping a trace by an outdated copy of its root span. When a run was exported more than once (for example after resuming), aggregateTraces() could count the same trace under both status: error and status: success, and queryTraces() could list a trace by a filter that only its older copy matched. Every trace read now uses the root copy that ended last, before applying filters. (#27679)

  • Added support for grouping and filtering by the root span's name and spanType in the ClickHouse observability store. aggregateTraces() can group by both and count their distinct values, and queryTraces(), aggregateTraces(), and trace-query value discovery can filter on or suggest them. Values come from each trace's current completed root span. The store declares the trace-query-root-span-fields storage feature. (#27321)

    await mastraClient.aggregateTraces({ timeRange, groupBy: ['spanType', 'name'], measures: ['count'] });

@mastra/client-js@1.56.0

Minor Changes

  • Breaking: controller.session() takes a session id, and the resource moves to create(). The scope argument is removed. (#27336)

    // Before
    const session = client.getAgentController('code').session('repo-1', 'run-a');
    await session.create();
    
    // After
    const session = client.getAgentController('code').session('run-a');
    await session.create({ resourceId: 'repo-1' });

    create() without a resourceId resumes the session on the resource it already has, or uses the session id as the resource for a new session, so session('user-1').create() behaves as before.

  • Added experimental methods for MCP server events: (#27674)

    const { events } = await client.getMcpServerEvents('tickets');
    const { events: occurrences, cursor } = await client.pollMcpServerEvent('tickets', 'ticket.created', {
      arguments: { project: 'web' },
    });
    
    const controller = new AbortController();
    for await (const frame of client.streamMcpServerEvent(
      'tickets',
      'ticket.created',
      { cursor },
      { signal: controller.signal },
    )) {
      if (frame.type === 'event') console.log(frame.occurrence.data);
    }
    
    // Under `mastra dev` only
    await client.emitMcpServerEvent('tickets', 'ticket.created', { title: 'Test ticket' });
  • Added thread rows, activity sorting and direct filters to queryTraceThreads(). (#27652)

    Breaking type change: QueryTraceThreadsResult now describes full thread rows. Use QueryTraceThreadsResult<typeof request> for legacy identity results.

    // Before
    await client.queryTraceThreads({ traces: { timeRange: { from, to } } });
    // After
    await client.queryTraceThreads({ timeRange: { traces: { from, to } } });
  • Added context and parent/root entity fields to querySpans() response types. (#27272)

    Breaking: querySpans() response types no longer include cost, inputTruncated, outputTruncated and errorTruncated.

Patch Changes

  • Fixed client tools failing with ToolNotFoundError after a tool approval. approveToolCall, declineToolCall, approveToolCallGenerate and declineToolCallGenerate now accept clientTools (and clientToolsResolver) and send them to the server, so the model can call a client tool after the approval decision. approveToolCall and declineToolCall also accept memory; pass the same thread and resource as the original call so client-tool results and the final answer are saved to that thread. (#27744)

    const stream = await agent.approveToolCall({
      runId,
      toolCallId,
      clientTools,
      memory: { thread: threadId, resource: resourceId },
    });

    Also in: @mastra/server@1.79.0

  • Added typed support for the new agent controller thread ownership route (POST /agent-controller/:controllerId/sessions/:resourceId/threads/:threadId/owner). (#27043)

    POST /agent-controller/code/sessions/resource-1/threads/thread-1/owner
    → { "ok": true, "ownerId": "user-123" }
  • Fixed a security alert when you install @mastra/client-js. The package no longer depends on @ai-sdk/ui-utils, which included a vulnerable version of @ai-sdk/provider-utils. Streaming works the same as before. (#27297)

  • Fixed approveToolCall, declineToolCall, and resumeStream logging tool_result must be preceded by a tool_call on every resume. Client tools the model calls after a resumed tool call now run as expected. (#27207)

  • Span types returned by the trace and span endpoints now include timeToFirstChunkMs, the milliseconds from span start to the first streamed chunk. (#27140)

  • Agent controller session subscriptions now detect silent connections instead of hanging forever. When a stream receives no data (including server heartbeats) for idleTimeoutMs (default 60 seconds), it's treated as dropped, so reconnect and onReconnect re-establish it, or onError fires when reconnect is off. Opening a stream that never responds is bounded by the same limit. Set idleTimeoutMs: 0 to disable. (#26904)

    const subscription = await session.subscribe({
      onEvent: handleEvent,
      onReconnect: resync,
      reconnect: true,
      idleTimeoutMs: 60_000,
    });
  • Trace query requests (queryTraces, aggregateTraces, querySpans, queryTraceThreads) no longer retry when the server reports a query execution timeout (504 with code TRACE_QUERY_EXECUTION_TIMEOUT). Retrying re-ran the same expensive query against the same timeout and could make a failing call take over a minute. Other 5xx errors, including 504s from proxies or gateways, are still retried. (#27385)

  • Added modelListEditable to agent details so clients can disable model-list editing when an agent resolves its models dynamically. Studio now shows function-valued model lists as read-only while keeping static model lists editable. (#27192)

    const agent = client.getAgent('weather-agent');
    const details = await agent.details();
    
    if (details.modelListEditable === false) {
      // Hide or disable model-list editing controls.
    }

    Also in: @mastra/server@1.79.0

  • Trace queries, trace aggregates, and thread queries now return a 422 when they use the root span's entityId at trace scope and the configured observability store does not support it, instead of failing with a 500. For those stores, trace-query field discovery also omits trace-scope entityId from its field list and aggregate.dimensions. A new traceQueryRootEntityId capability reports whether the store supports it. Span-scope entityId inside spans.some keeps working on every store. (#27684)

    const { capabilities } = await mastraClient.getObservabilityCapabilities();
    if (capabilities.traceQueryRootEntityId) {
      await mastraClient.aggregateTraces({ timeRange, groupBy: ['entityId'], measures: ['count'] });
    }

    Also in: @mastra/server@1.79.0

  • Trace queries, trace aggregates, and thread queries now return a 422 when they use the root span's name or spanType and the configured observability store does not support them, instead of failing with a 500. For those stores, trace-query field discovery also omits both fields from its field list and aggregate.dimensions. A new traceQueryRootSpanFields capability reports whether the store supports them. (#27321)

    const { capabilities } = await mastraClient.getObservabilityCapabilities();
    if (capabilities.traceQueryRootSpanFields) {
      await mastraClient.aggregateTraces({ timeRange, groupBy: ['name'], measures: ['count'] });
    }

    Also in: @mastra/server@1.79.0

  • The schedules HTTP API now accepts and returns ifIdle.requireClaimedOwner on agent schedules. Before this, POST /schedules and PATCH /schedules/:scheduleId dropped the flag, so a client that read a schedule and sent its ifIdle back cleared it. (#27896)

    await client.createSchedule({
      agentId: 'support',
      cron: '0 9 * * *',
      prompt: 'Morning check-in',
      threadId: 'thread-1',
      resourceId: 'user-1',
      ifIdle: { behavior: 'wake', requireClaimedOwner: true },
    });

    Also in: @mastra/server@1.79.0

  • Fixed client tools that run after approveToolCall / declineToolCall on an agent without memory. The follow-up model call now sees the whole conversation: the original user message, the approved (or declined) tool call and its result. Before, it saw only the client tool call and its result. (#27772)

    approveToolCallGenerate and declineToolCallGenerate now run executable client tools the model calls after the decision, and then continue the conversation, the same way generate does. Before, they returned with finishReason: 'tool-calls' and never ran the client tool.

    Pass memory to approveToolCallGenerate / declineToolCallGenerate to save that continuation to the thread:

    await agent.approveToolCallGenerate({
      runId,
      toolCallId,
      clientTools,
      memory: { thread: threadId, resource: resourceId },
    });
  • The Agent Controller tool-approval endpoint now accepts alwaysAllowCategory: true, which approves the call and grants its tool category to the call's thread, the same way "always allow" works in an in-process session. Pass it from @mastra/client-js with session.approveTool(toolCallId, true, { alwaysAllowCategory: true }). (#27222)

    Also in: @mastra/server@1.79.0

  • Added timeToFirstChunkMs to the queryTraces() and querySpans() result types, and the traceQueryTimeToFirstChunk flag to getObservabilityCapabilities(). (#27277)

    const { capabilities } = await mastraClient.getObservabilityCapabilities();
    
    if (capabilities.traceQueryTimeToFirstChunk) {
      const { spans } = await mastraClient.querySpans({
        timeRange,
        where: { op: 'gt', left: { path: 'timeToFirstChunkMs' }, right: { literal: 2000 } },
      });
    }
  • Added spanQueryContextFields to observability capability response types. (#27807)

  • queryTraces() and querySpans() now accept timeToFirstChunkMs in orderBy, and getObservabilityCapabilities() returns the traceQueryTimeToFirstChunkSort flag. (#27473)

    const { capabilities } = await mastraClient.getObservabilityCapabilities();
    
    if (capabilities.traceQueryTimeToFirstChunkSort) {
      const { spans } = await mastraClient.querySpans({
        timeRange,
        orderBy: [{ field: 'timeToFirstChunkMs', direction: 'desc' }],
      });
    }

@mastra/code-sdk@1.15.0

Minor Changes

  • Added an outputProcessors option to createMastraCode. Output processors you pass run after Mastra Code's built-in ones and before memory saves the reply, so changes they make to response messages are persisted. (#27162)

    const mastraCode = await createMastraCode({
      outputProcessors: ({ requestContext }) => [new ReplyLabelProcessor(requestContext)],
    });
  • Added a Factory client for driving a remote Factory session. RemoteSession and RemoteController mirror a Factory session's state over its event stream and forward messages, approvals, and mode/model switches over HTTP. Platform helpers sign in to Mastra Platform and discover the organizations, projects, and environments to connect to. (#27222)

    import { createFactoryClient } from '@mastra/code-sdk/factory/client';
    import { RemoteSession } from '@mastra/code-sdk/factory/remote-session';
    import { getPlatformSession } from '@mastra/code-sdk/platform/index';
    
    const session = await getPlatformSession();
    const client = createFactoryClient({ url: 'https://my-factory.factory.mastra.cloud', session });

    Requests that fail with an expired Platform token refresh it once and retry.

  • SandboxFilesystem now uses the sandbox's native filesystem when it has one. File reads, writes, stats, and listings take one request instead of two or three shell commands, and every file operation goes through it. grep and file walks still run as shell commands. Paths with .. are still rejected. (#27859)

    On the native path, the in-sandbox symlink check is skipped by default, since an agent with a shell can read any file anyway. Set verifySymlinks: true when you serve files to people who can't run commands; each path is then checked once per instance:

    const fs = new SandboxFilesystem({ sandbox, workdir: '/workspace/repo', verifySymlinks: true });
  • Added resolveDefaultThinkingLevel so a host can supply the thinking level for requests whose session has not picked one. When set, it replaces the settings.json defaults; a session's own level still wins. It runs on every model resolution, including account-rotation hops, so a changed default applies to the next request. (#27216)

    const { controller } = await createMastraCodeAgentController({
      resolveDefaultThinkingLevel: async ({ requestContext }) => lookUpTeamThinkingLevel(requestContext),
    });

Patch Changes

  • Fixed ACP turns that an internal error ended returning stopReason: "cancelled" when the client never cancelled. session/prompt now returns a JSON-RPC error with the underlying message, so clients can tell a failure apart from a cancel. (#27438)

  • Background task completions and notification wake-ups now find their session by session id, so they keep reaching the right session when several sessions share one resource. (#27336)

  • Fixed the local trace store (observability.duckdb, used when /observability local on) growing without limit. Each session now deletes traces, logs and metrics older than 14 days and scores and feedback older than 30 days in the background, capped per session so startup is never blocked. (#27506)

  • Fixed sandbox workspace search staying on the slow fallback for the whole session when its first search ran before the sandbox was ready. A failed search-tool check is now retried on the next search. (#27695)

  • Credential resolution failures (for example a run with no signed-in user) are now logged as errors through the Mastra instance's logger, so they appear in deployed logs. The missing-user error also includes the session and thread ids and the names of the request context entries, so the code path that started the run can be identified. (#27376)

  • Factory sessions now always use thread-scoped observational memory. Each run and chat keeps its own observations, and recall search still reaches every thread under the Factory or user resource. Session state, the MASTRA_OM_SCOPE environment variable, and .mastracode/database.json no longer change the scope for Factory sessions. Mastra Code outside Factory resolves the scope as before. (#27337)

  • Fixed notifications for the thread you have open being treated as belonging to another project when the thread's recorded project path differs from the current checkout. Such notifications now run in your session instead of failing with "No model available". If a Mastra Code process for the thread's own project is also running, only one of the two delivers each notification. (#27634)

  • Fixed "build a workflow" requests never finishing in Mastra Code. The workflow builder no longer inherits the chat session's memory thread, so it stops waiting on the session run that is waiting on it. (#27323)

  • Added normalizeLegacyProviderId to @mastra/code-sdk/providers/model-ids. It maps a provider id Mastra Code once defined itself to the catalog id models.dev uses, so a reply's provider can be matched against the catalog. (#27162)

    import { normalizeLegacyProviderId } from '@mastra/code-sdk/providers/model-ids';
    
    normalizeLegacyProviderId('kimi-for-coding'); // 'kimi-code-plan-cn'
    normalizeLegacyProviderId('anthropic'); // 'anthropic'
  • Fixed headless --output jsonl runs writing gigabytes of repeated data. The stream included a display_state_changed event after almost every other event, and each one carried a full copy of the session's display state, including every large tool result and all shell output so far. Long runs could fill the disk. (#26881)

    --output jsonl no longer writes display_state_changed events. Every other event, and the final result line, is written as before. The omitted snapshots only repeated what the other events already carry. Consumers that rebuilt UI state from these snapshots should fold the remaining events instead. --output json, --output human, and programmatic runMC() iteration are unchanged.

  • Fixed a prune() failed for domain "observability" error being logged the first time local tracing opened a new observability.duckdb file. Mastra Code now skips the startup prune until the trace tables exist; the next session prunes as usual. (#27517)

  • Auto memory now picks Gemini only when Mastra Code can use a Google credential for the request: a stored key or sign-in, or locally a GOOGLE_GENERATIVE_AI_API_KEY or GOOGLE_API_KEY environment variable. Deployed Factory accounts never use the server's environment keys, so they get a model from a provider they connected. (#26666)

  • /om now shows the same Auto memory model that runs use. Before, the preview only checked GOOGLE_GENERATIVE_AI_API_KEY, so with GOOGLE_API_KEY or a Google key saved with mastracode login it showed Claude Haiku while runs used Gemini Flash. (#27449)

  • Fixed foreground commands in Mastra Code to stop after 30 seconds unless the command specifies a timeout. Set timeout to 0 to disable the limit. Background processes are unaffected. (#26878)

  • Improved content search in sandbox workspaces without ripgrep. When the sandbox's GNU grep and git support PCRE, patterns run with grep -P and git grep -P, so \d, \w, \s, \b, and lookarounds are searched in one command instead of falling back to reading files one by one. (#27698)

  • Fixed headless runs reporting success while a tool was still waiting for an answer. When a tool such as request_access paused the run, runMC stopped with status: 'completed' and exit code 0, so the answer from the permission policy was never applied. Headless runs now wait for the paused run to stop, send the policy's answer, and keep running until the resumed run finishes. If a paused tool can't be resumed, the run ends with status: 'error' and exit code 1 instead of reporting success. (#26883)

  • Fixed headless runs cutting off their output when stdout is a pipe. The CLI exited before Node finished writing, so a large final result could reach the reader incomplete while the process still exited with code 0. Headless mode now waits for stdout and stderr to flush before exiting. If the output can't be delivered because the reader closed the pipe, a run that would otherwise succeed exits with code 1 instead of 0. A run that already failed keeps its exit code. (#26880)

  • Improved sandbox grep reliability and diagnostics. When rg or grep fails inside the sandbox, the error now says why, for example a timeout or an exit code with the first stderr line. Previously every failure was reported as an unsupported pattern. The POSIX grep fallback now skips .git, and it keeps matches when only some files could not be read. (#27464)

  • Scheduled prompts that fire into a session no longer held live in this process now run as that session's owner. Hosts that resume sessions (resumeWakeSession) bring the owner's session back first; if no owner can be found, the fire fails with No session owns resource … to run its wake on thread … before any run starts, instead of starting a run with no signed-in user that failed with "Tenant model credential policy could not be resolved: the request has no authenticated user." Only hosts with schedule tools enabled are affected. (#27780)

  • Made content search in sandbox workspaces much faster when ripgrep isn't installed. Searches now run as a single GNU grep command with context lines, using git to respect every repository's .gitignore, instead of reading files one by one. With ripgrep, searches also respect .gitignore files now. Both skip node_modules unless the search path points inside it. File listings in sandbox workspaces also skip node_modules inside the sandbox instead of returning it to be filtered out. (#27687)

  • Fixed the cross-agent signal sender telling the model to resend a signal that failed direct delivery. The signal waits in the recipient's inbox and is retried automatically, so the sender now tells the model not to resend it. A resend while that signal is still pending merges into it; a resend after it is delivered sends the recipient a second copy. (#27347)

  • Added Extra high thinking to xAI models that list it, such as Grok 4.6. (#26530)

    import { getModelReasoningOptions } from '@mastra/core/llm';
    import { getAvailableThinkingLevelsForModel } from '@mastra/code-sdk/thinking';
    
    getAvailableThinkingLevelsForModel('xai/grok-4.6', getModelReasoningOptions('xai/grok-4.6'));
    // ['off', 'low', 'medium', 'high', 'xhigh']
  • Mastra Code sessions no longer load web search providers, the Postgres store, the local trace store (DuckDB), the MCP client, local embeddings or voice SDKs until a feature needs them, which cuts startup memory by about 90 MB. (#27680)

  • Added a resumeWakeSession option to createMastraCode. It runs when a notification is due on a thread whose session isn't open in this process, so the host can bring that session back online as its owner before the wake runs. If no session owns the thread afterwards, or the hook throws, the delivery fails with that error instead of waking with no user. (#27729)

    Pair it with prepareWakeRequestContext, which attaches the owner's identity to the wake itself:

    const code = await createMastraCode({
      resumeWakeSession: async ({ resourceId, threadId }) => {
        const owner = await lookupSessionOwner(resourceId);
        if (!owner) throw new Error(`Session ${resourceId} has no known owner`);
        const requestContext = new RequestContext([['user', owner]]);
        await code.controller.createSession({ id: resourceId, resourceId, ownerId: owner.id, threadId, requestContext });
      },
      prepareWakeRequestContext: async ({ requestContext, resourceId }) => {
        requestContext.set('user', await lookupSessionOwner(resourceId));
      },
    });
  • Fixed woken notifications running in the wrong project when Mastra Code processes in different directories share one resource id. A due notification now runs as the session whose project the thread belongs to, and is delivered by a process running in that project: each project gets its own notification dispatch turn on the shared resource, so a process no longer runs another project's thread with its own workspace and tools, and no longer leaves it waiting until the process holding the resource exits. (#27426)

@mastra/connect@1.1.0

Minor Changes

  • Added Microsoft Teams as a supported Connect channel. When MASTRA_ENCRYPTION_KEY is unset, the Teams install store is encrypted with a key derived from the platform access token and a warning is logged; projects without a Teams connection are unaffected. (#27094)

  • Added end-user (per-resource) connections to @mastra/connect. (#26923)

    Scope tools to an end user

    tools() accepts a new resourceId option — a static key or a callback derived per request — so an agent resolves the shared global connections plus that end user's own connections. Every platform call made under a resource scope asserts the key, so one user's connection can never serve another user's request.

    import { tools } from '@mastra/connect';
    
    // Per-request scoping on an agent:
    new Agent({ tools: ctx => tools({ resourceId: myUserIdFrom(ctx) }) });
    
    // Static/global usage is unchanged:
    new Agent({ tools: tools({ providers: ['linear'] }) });

    Calling tools() per request is cheap: identical options share one internal snapshot/MCP-client state, with per-resource caching and eviction. The returned resolver is also awaitable, so a dynamic tools argument can return it directly.

    Let end users connect their own accounts

    A new userConnectRoutes() export returns mountable API routes (provider catalog, the caller's connections, connect sessions, status polling, disconnect, and a hosted connect page) gated on the authenticated end user's resource key from server.auth.mapUserToResourceId.

    import { Mastra } from '@mastra/core';
    import { userConnectRoutes } from '@mastra/connect';
    
    export const mastra = new Mastra({
      server: {
        auth: {
          authenticateToken: async token => verifyToken(token),
          mapUserToResourceId: user => user.id,
        },
        apiRoutes: [...userConnectRoutes({ providers: ['linear', 'slack'] })],
      },
    });

    credential() also gains an optional resourceId option for asserting a resource-scoped connection's key.

  • Added a callback form to tools() for per-request options. Pass a function that reads the request context and returns ToolsOptions to vary tool filters and approval policies per request (for example by user role), without losing the snapshot cache: resolutions producing the same options share one cached snapshot with the usual TTL semantics. (#27160)

    Before (per-request resolver, re-fetches every request):

    tools: async ctx => {
      const role = ctx.requestContext?.get('role');
      return tools({
        providers: {
          linear: role === 'viewer' ? { allowTools: ['linear_list_*'] } : { requireApproval: ['linear_delete_*'] },
        },
      })(ctx);
    };

    After (one resolver, cache intact):

    tools: tools(ctx => ({
      providers: {
        linear:
          ctx?.requestContext?.get('role') === 'viewer'
            ? { allowTools: ['linear_list_*'] }
            : { requireApproval: ['linear_delete_*'] },
      },
    }));

Patch Changes

  • Fixed project-level Connect activity showing empty on the Mastra platform. @mastra/connect now sends its configured project id (MASTRA_PROJECT_ID or the projectId option) with every proxied provider request, so the platform can attribute tool and channel activity to the right project. (#26932)

    import { tools } from '@mastra/connect';
    
    // Uses MASTRA_PROJECT_ID by default; pass projectId to override.
    const agentTools = tools({ projectId: 'proj_123' });
  • Google Drive (#27156)

    • Fixed google_drive_create_folder, google_drive_upload_document, and google_drive_move_file failing with 404 errors on shared drives. Agents can now write to team shared drives.
    • Added google_drive_export_file for exporting Docs, Sheets, and Slides to PDF and other formats.
    • Added google_drive_list_revisions for listing a file's revision history.
    const toolset = await tools({ providers: ['google-drive'] })();
    
    await toolset.google_drive_export_file.execute({
      fileId: '1Kj3d86Z-Sfd56YP4dImQ-ggMRyP2QZ_BRO33zOO224c',
      mimeType: 'application/pdf',
    });

    Fireflies

    • Fixed fireflies_get_transcript failing validation on every transcript. Fireflies returns numeric speaker_id, start_time, and end_time values that the schema only accepted as strings; the tool now accepts and normalizes them.
  • @mastra/mcp and @mastra/connect now require @mastra/core 1.79.0 or later. Upgrade @mastra/core together with them: (#27674)

    # Before: @mastra/core 1.78.x with an older @mastra/mcp
    npm install @mastra/mcp@latest # peer dependency error
    
    # After
    npm install @mastra/core@latest @mastra/mcp@latest @mastra/connect@latest

    Also in: @mastra/mcp@2.5.0

@mastra/convex@1.7.5

Patch Changes

  • Fixed restarted foreach runs with nested workflows re-running already completed child steps when using Convex storage. (#26368)

@mastra/datadog@1.4.18

Patch Changes

  • Simplified agentless-mode defaults while preserving configuration and environment handling. (#27646)

@mastra/deployer@1.79.0

Patch Changes

  • Fixed generated servers exiting immediately instead of shutting down gracefully on Ctrl-C under mastra dev, mastra start, or another tool that forwards the signal. The server receives Ctrl-C twice in a few milliseconds, once from the terminal and once from the parent, and treated the second as a request to force-quit. A second signal within 250ms of the first is now ignored, so in-flight requests drain and Mastra shuts down cleanly. Pressing Ctrl-C again after that still exits immediately. (#26986)

  • Fixed mastra build and mastra dev failing in monorepos where multiple workspace packages share code. (#27204)

  • Servers built with mastra build now start model usage telemetry before serving requests, so shutdown reports include every model call the server handled. (#26986)

  • Fixed dev and build failures when projects use TypeScript 7. (#27840)

  • Removed the find-workspaces and gray-matter dependencies. Monorepo workspace discovery now uses tinyglobby and markdown frontmatter is parsed by a helper shared with @mastra/core. This removes the vulnerable braces (GHSA-vfj7-8cjw-p6xm) and sprintf-js (GHSA-hp3w-g68c-fv3c) packages, neither of which has a patched release, from the production dependency tree of @mastra/deployer. npm audit no longer reports them for this package. (#27198)

  • Fixed mastra dev failing when a server.apiRoutes handler or bundler/deployer option imports a workspace package that ships raw TypeScript. These packages are now inlined into the extracted config instead of being left as imports that Node cannot load. Their own dependencies still resolve from the workspace package, so dependencies installed only for that package (for example with pnpm) keep working. (#27303)

  • Agent browser streaming now requires authentication. The browser WebSocket stream (/browser/:agentId/stream), the session probe, and the close endpoint previously accepted unauthenticated requests; they now require the same authentication as the rest of the server. (#26112)

    Requests without a valid credential for the server auth provider configured on Mastra are denied with 401.

    Browser WebSocket upgrades authenticate with the session cookie that same-origin requests already send, so signed-in Studio sessions keep working. A WebSocket handshake cannot set an Authorization header, so a client that authenticates with a header first exchanges its credential for a single-use upgrade ticket over an authenticated HTTP request:

    const response = await fetch(`${apiUrl}/agents/my-agent/browser/stream-ticket`, {
      method: 'POST',
      headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ threadId }),
    });
    const { ticket } = await response.json();
    
    const ws = new WebSocket(`wss://example.com/browser/my-agent/stream?threadId=${threadId}&ticket=${ticket}`);

    The ticket is opaque, expires after 30 seconds, is bound to the agent and thread it was issued for, and is consumed by the first upgrade it authenticates. The credential is only ever presented to the mint endpoint, so it never reaches a proxy or an access log, and each connection attempt mints a fresh ticket.

    Issuance is bounded so the mint endpoint cannot be used to exhaust the server: each agent may request a limited number of tickets per minute and the process holds a capped number of unspent tickets, beyond which the endpoint answers 429 instead of allocating more.

    For clients that cannot call the mint endpoint, ?apiKey= remains available as a handshake-only fallback:

    const ws = new WebSocket(`wss://example.com/browser/my-agent/stream?threadId=${threadId}&apiKey=${token}`);

    A token in a URL can be retained in proxy and access logs, so prefer the ticket flow, and pass a short-lived token if you use this fallback. The mint endpoint, the session probe, and the close endpoint also accept the token through the Authorization header.

    Studio's browser view follows the same rule: when Studio is configured with an Authorization header it requests an upgrade ticket for each connection, so a deployment that authenticates with a header keeps its live view instead of losing it to a 401. Deployments that sign users in with a session cookie are unaffected.

    setupBrowserStream also accepts an optional allowedOrigins allowlist, in the same shape as Hono's cors.origin, and refuses an upgrade from any other origin with 403 before authentication runs. Upgrades from the server's own origin are always allowed, since a CORS allowlist names other origins and Studio is often served by the same server; this check compares the host and not the scheme. The deployer forwards server.cors.origin when it is explicitly configured.

    Direct callers of setupBrowserStream: the config now requires the mastra instance so the browser routes can be authenticated.

    Before:

    await setupBrowserStream(app, { getToolset });

    After:

    await setupBrowserStream(app, { mastra, getToolset });

    Also in: @mastra/hono@1.7.21

  • Fixed mastra build failing with "<export>" cannot be exported from ".mastra/.build/<pkg>.mjs" as it is a reexport that references itself when the app only reaches a workspace package through another workspace package, and those packages don't declare a version. (#27739)

  • mastra build now succeeds when your app imports a private workspace package that has no version field. (#27057)

@mastra/deployer-netlify@1.2.38

Patch Changes

  • Netlify installs now apply bundler.pnpmWorkspaceSettings and packed workspace dependency pnpmOverrides to the generated pnpm workspace, for both edge and serverless targets. (#27708)

@mastra/docker@0.12.0

Minor Changes

  • Added build progress reporting to DockerTemplate.build(). Pass onProgress to receive step starts and completions (including cache hits), raw command output, and a reused event when an existing image is used. Failed results now include failedStep, the most recent output (capped by maxOutputBytes, default 64 KiB), and outputTruncated. DockerSandbox accepts onTemplateBuildProgress to follow template builds triggered by start(). (#26682)

    const result = await template.build({
      onProgress: event => {
        if (event.type === 'step-start') console.log(event.step.name, event.step.cached ? '(cached)' : '');
      },
    });
    if (result.status === 'failed') console.error(result.failedStep?.name, result.output);

Patch Changes

  • Fixed kill() on Podman. Before, kill() returned false and the process group kept running, so command timeouts, aborts and background process kills had no effect. The kill helper now attaches its output streams, which Podman's Docker-compatible API requires. Kills now stop the process group on both Docker and Podman. (#27292)

@mastra/duckdb@1.15.0

Minor Changes

  • Added querySpans() support for additional context filters and response fields, including thread filtering: (#27272)

    where: { op: 'eq', left: { path: 'threadId' }, right: { literal: 'thread-123' } }

    Breaking with the updated core contract: querySpans() no longer accepts model and provider filters or returns cost, inputTruncated, outputTruncated and errorTruncated.

    Parent/root entity type, ID and name, and executionSource return null. Equality filters on these fields match no spans.

  • Added thread rows with previews, trace counts, activity timestamps, filters and cursor pagination. Legacy thread identity queries remain supported. (#27652)

    import { planThreadRowsQuery } from '@mastra/core/storage';
    
    await storage.queryThreads(
      planThreadRowsQuery({
        timeRange: { traces: { from, to } },
      }),
    );

    Also in: @mastra/pg@1.32.0

  • Added time to first chunk to DuckDB trace and span queries. queryTraces() returns the root span's timeToFirstChunkMs and querySpans() returns each span's, or null when no chunk was streamed. Both accept timeToFirstChunkMs filters. (#27277)

    where: { op: 'gt', left: { path: 'timeToFirstChunkMs' }, right: { literal: 2000 } }
  • DuckDB trace and span queries can now sort by timeToFirstChunkMs, slowest or fastest first. Traces and spans without a value come last. (#27473)

    orderBy: [{ field: 'timeToFirstChunkMs', direction: 'desc' }];

Patch Changes

  • Added support for grouping and filtering by the root span's name and spanType in the DuckDB observability store. aggregateTraces() can group by both and count their distinct values, and queryTraces(), aggregateTraces(), and trace-query value discovery can filter on or suggest them. Values come from each trace's current completed root span. The store declares the trace-query-root-span-fields storage feature. (#27321)

    await mastraClient.aggregateTraces({ timeRange, groupBy: ['spanType', 'name'], measures: ['count'] });

@mastra/e2b@0.15.0

Minor Changes

  • E2BSandbox.filesystem reads and writes the sandbox's disk through E2B's files API. Each operation starts the sandbox if needed and retries once when E2B paused or killed it server-side. (#27859)

    const sandbox = new E2BSandbox();
    await sandbox.filesystem.writeFile('/home/user/notes.md', '# Notes');
    const entries = await sandbox.filesystem.readdir('/home/user');

    Copies, appends, and writes or moves with overwrite: false run as one command inside the VM, so they're atomic and file contents don't travel through your server.

@mastra/elysia@0.1.19

Patch Changes

  • Fixed /api/openapi.json containing $refs that point nowhere. Recursive (z.lazy) schemas and schemas tagged with .meta({ id }) used to emit #/definitions/... or #/$defs/... refs that broke once inlined into the document. Those definitions now go into components.schemas and the refs point there, so tools like Swagger UI, swagger-parser, and OpenAPI client generators can load the spec. This covers both built-in and custom routes. When two different schemas share an id, the second one gets a suffixed name (for example Invoice_2) and neither overwrites the other. (#27745)

    Also in: @mastra/server@1.79.0

@mastra/evals@1.10.8

Patch Changes

  • Fixed the code tool-call accuracy scorer returning 0 in non-strict mode when expectedToolOrder contains the same tool more than once (for example search → inspect → search). Each expected tool now matches a separate call, and extra calls between matches are still allowed. (#27483)

  • Fixed noRedundantCalls on trajectories built from traces. Two identical tool calls in a row are now flagged even when processor_run or model_generation steps sit between them, so scoring a run from its trace gives the same result as scoring it from its messages. (#27316)

  • Fixed blacklistedSequences and maxRetriesPerTool on trajectories built from traces. Forbidden tool sequences and tool retries are now detected even when processor_run or model_generation steps sit between the tool calls, so scoring a run from its trace gives the same result as scoring it from its messages. (#27387)

  • Fixed trajectory scorers giving wrong results on trajectories built from traces. Traces wrap tool calls in model_generation steps and put processor_run steps around them, so a correct run scored 0 for accuracy, maxSteps counted wrapper steps, and forbidden sequences and tool retries were missed. All trajectory checks (compareTrajectories, checkTrajectoryEfficiency, checkTrajectoryBlacklist, analyzeToolFailures, the code trajectory scorers, and the LLM trajectory judge) now read tool calls the same way for trace and message trajectories, so a run gets the same score either way. (#27487)

    Token budgets still read model_generation steps, and expectations that name model_generation or processor_run steps are still compared against the raw trajectory. maxSteps and the index in redundantCalls now count tool-level steps on trace trajectories.

    The new toToolLevelSteps helper returns this tool-level view if you need it in a custom scorer.

    import { toToolLevelSteps } from '@mastra/evals/scorers/utils';
    
    const toolNames = toToolLevelSteps(trajectory.steps).map(step => step.name);

    Retries are now grouped across model_generation steps of the same run, as they are on message trajectories. Tool calls in separate sub-agent runs are still counted separately.

@mastra/factory@0.24.0

Minor Changes

  • Factory user sessions can now start sub-sessions and hear back from them. (#27103)

    • factory_start_session starts a side session with a prompt, or a role on a work item (like pressing Start, without moving the card). New sessions run as the caller, with the caller's credentials, and the caller follows the work item.
    • Spawned sessions report with factory_message_origin. A side session reports to the session that started it; a work-item session reports to everyone following the item. failed and blocked reports are urgent, done is high priority, and progress updates replace each other while unread.
    • Every run of a work session or side session ends with exactly one final report, however the run started (the board, a typed message, a wake, or after a restart): if the agent doesn't report, Factory sends done, failed, or blocked when the run ends, including when it stops waiting for someone to approve a tool call.
    • When a work session or side session holds a tool call for approval, whoever follows it gets an urgent approval notification right away and can approve or decline it with factory_answer_approval.
    • Followed items send a low-priority notice to running sessions when they change stage.
    • A <factory-sessions> state signal shows a session the side sessions it started and the work items it follows, once there is something to show.
  • Moved the thinking level default onto each Factory. Organization admins set it under Settings → Models → Factory defaults, and every session and work-item run in that Factory uses it until it picks its own level. Every Factory, including existing ones, starts at medium after upgrading. (#27216)

    Why: the old default lived in the server's settings.json, so one value was shared by every Factory on the server, and with sign-in turned on it could only be changed after setting MASTRACODE_DEPLOYMENT_ORGANIZATION_ID.

    Removed: the GET /web/config/thinking and PUT /web/config/thinking routes and the MASTRACODE_DEPLOYMENT_ORGANIZATION_ID variable. Set the level on the Factory instead:

    # Before
    curl -X PUT "$FACTORY_URL/web/config/thinking" -H 'Content-Type: application/json' -d '{ "globalDefault": "high" }'
    
    # After
    curl -X PATCH "$FACTORY_URL/web/factory/projects/$FACTORY_ID" -H 'Content-Type: application/json' -d '{ "defaultThinkingLevel": "high" }'

    A level saved in settings.json (global or per mode) is no longer read by Factory sessions, so if you set one there, set it again on each Factory. The per-chat "Chat defaults" row is also gone; a level picked there still overrides the Factory default for the chats that used it, so run /think default in those chats to follow the Factory default.

  • Treat linked Slack conversations as User Sessions, with interactive User Session tools authorized as the linked sender, including other participants in an organization-visible session. Private sessions remain owner-only. (#27685)

    Before: a linked teammate replying in a Slack session could not use User Session management tools without acting as the session owner. After: they can ask, for example, “Switch my other session to openai/gpt-6-luna,” and factory_switch_session_model checks their own session ownership rather than the Slack session owner's.

  • Added a built-in MCP server to Factory at /web/factory/projects/:projectId/mcp. MCP clients such as ChatGPT can sign in with OAuth and use the Factory tools (work items, sessions, decisions, approvals, plus search and fetch) with the same permissions as the web UI. Factories on Mastra Platform auth sign users in through Platform automatically; other auth providers set mcp.authorizationServers (or MASTRA_FACTORY_MCP_AUTHORIZATION_SERVER). Pass mcp: false to turn it off. (#27228)

    new MastraFactory({
      // ...
      mcp: { authorizationServers: ['https://your-tenant.authkit.app'] },
    });
  • Factory sessions now share memory. A Factory's work-item runs, side sessions, and Slack channel threads store their threads under the Factory id. The sessions a person opens, and their Slack DMs, store theirs under that person's user id, so a DM is never shared with the Factory's runs. Factory sessions always use thread-scoped observational memory, and session state, MASTRA_OM_SCOPE, and `.mastracod

Don't miss a new mastra release

NewReleases is sending notifications on new releases.