@mastra/core@1.79.0

mastra-ai/mastra@mastra/core@1.79.0Oct 21, 2026by Patrycja-J

Upgrading?

AI Summary

Mastra core 1.79.0 adds experimental MCP Events with Studio inspection, major observability upgrades including span aggregation and time-to-first-chunk metrics, and a shift to session-ID-based Agent Controller sessions. It also introduces a native sandbox filesystem API and end-user scoped Connect integrations with Microsoft Teams support, alongside several breaking API and storage contract changes.

Key Highlights

  • Experimental MCP Events (SEP-3415 draft) support polling, streaming, and signed webhooks, with Studio inspection APIs under `mastra dev`.
  • Observability upgrades add the `aggregateSpans()` storage contract, `timeToFirstChunkMs` query fields, richer trace/span filtering, and root entity trace-scope fields.
  • Agent Controller sessions are now addressed by `sessionId`, enabling multiple sessions per `resourceId` across core, server, and client-js.
  • `WorkspaceSandbox` can expose an optional native `filesystem` API for faster one-roundtrip reads, writes, and listings.
  • `@mastra/connect` adds per-resource connection scoping, `userConnectRoutes()` for end-user account connections, and Microsoft Teams as a supported channel.

Breaking Changes

  • Agent Controller sessions are keyed by `sessionId` instead of `resourceId + scope`; controller APIs, server routes, and client-js session signatures were updated.
  • Thread-row query contracts changed to use `timeRange.traces` and `timeRange.threads` instead of `traces.timeRange`.
  • `querySpans()` removed `model` and `provider` filters and dropped `cost`, `inputTruncated`, `outputTruncated`, and `errorTruncated` from responses, requiring updated storage adapters.
  • Channel `resolveResourceId` and `resolveThreadId` contexts now require `requestContext`.
  • Cross-process `DurableAgent.observe(runId)` now throws `DURABLE_AGENT_OBSERVE_AGENT_MISMATCH` when the persisted run belongs to an agent with a different ID.

New Features

  • Experimental MCP Events support for poll, stream, and webhook delivery, plus Studio inspection of event types, occurrences, and test emissions.
  • New `aggregateSpans()` storage contract and `span-aggregate` feature for per-span analytics such as p95 latency per tool or token/cost per agent.
  • Observability queries now support `timeToFirstChunkMs` fields, filtering, sorting, and expanded trace-scope fields including root `entityId`, root `name`, and root `spanType`.
  • `querySpans()` gains richer context, parent, and root-entity filters with updated storage capability detection.
  • Agent Controller adds `controller.getSession(sessionId)`, updated `createSession()` and `deleteSession()` signatures, and server routes under `/agent-controller/:controllerId/sessions/:sessionId/...`.
  • `WorkspaceSandbox` optional native filesystem API for faster workspace operations, adopted by `SandboxFilesystem` in `@mastra/code-sdk`.
  • `@mastra/connect` adds per-resource end-user connection scoping, `userConnectRoutes()`, and Microsoft Teams channel support.
  • `aggregateTraces()` can group by root span `entityId`, and trace queries accept `entityId` as a trace-scope `where` field.
  • `DurableAgent.observe(runId)` fixes restore configured output processors, request context, and scorer data, and makes durable stream `onChunk` callbacks run serially and in order.

Full Release Notes

## 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](https://github.com/mastra-ai/mastra/blob/@mastra/core@1.79.0/packages/core/CHANGELOG.md)
#### 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`.

  ```ts
  // 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)

  ```ts
  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)

  ```ts
  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`:

  ```ts
  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`:

  ```ts
  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)

  ```ts
  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)

  ```ts
  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)

  ```ts
  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)

  ```ts
  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:

  ```ts
  // 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.

  ```ts
  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)

  ```ts
  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.

  ```ts
  // 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.

  ```ts
  // 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)

  ```ts
  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)

  ```ts
  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)

  ```ts
  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.

  ```ts
  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)

  ```ts
  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)

  ```ts
  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)

  ```typescript
  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)

  ```ts
  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)

  ```ts
  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)

  ```ts
  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)

  ```ts
  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:

  ```ts
  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.

  ```ts
  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)

  ```ts
  // 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)

  ```ts
  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)

  ```ts
  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:

  ```ts
  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)

  ```ts
  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)

  ```ts
  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)

  ```ts
  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.

  ```ts
  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)

  ```ts
  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:

  ```ts
  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](https://github.com/mastra-ai/mastra/blob/@mastra/acp@0.4.3/agent-sdks/acp/CHANGELOG.md)
#### 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](https://github.com/mastra-ai/mastra/blob/@mastra/astra@1.1.2/stores/astra/CHANGELOG.md)
#### Patch Changes

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

### [@mastra/clickhouse@1.26.0](https://github.com/mastra-ai/mastra/blob/@mastra/clickhouse@1.26.0/stores/clickhouse/CHANGELOG.md)
#### 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)

  ```typescript
  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)

  ```typescript
  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.

  ```ts
  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)

  ```ts
  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)

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

### [@mastra/client-js@1.56.0](https://github.com/mastra-ai/mastra/blob/@mastra/client-js@1.56.0/client-sdks/client-js/CHANGELOG.md)
#### Minor Changes

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

  ```ts
  // 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)

  ```ts
  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.

  ```ts
  // 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)

  ```ts
  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)

  ```http
  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)

  ```ts
  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)

  ```ts
  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)

  ```ts
  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)

  ```ts
  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)

  ```ts
  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:

  ```ts
  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)

  ```ts
  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)

  ```ts
  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](https://github.com/mastra-ai/mastra/blob/@mastra/code-sdk@1.15.0/mastracode/sdk/CHANGELOG.md)
#### 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)

  ```ts
  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)

  ```ts
  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:

  ```ts
  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)

  ```ts
  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)

  ```ts
  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)

  ```ts
  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:

  ```ts
  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](https://github.com/mastra-ai/mastra/blob/@mastra/connect@1.1.0/packages/connect/CHANGELOG.md)
#### 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.

  ```ts
  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`.

  ```ts
  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):

  ```ts
  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):

  ```ts
  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)

  ```typescript
  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.

  ```ts
  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)

  ```bash
  # 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](https://github.com/mastra-ai/mastra/blob/@mastra/convex@1.7.5/stores/convex/CHANGELOG.md)
#### 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](https://github.com/mastra-ai/mastra/blob/@mastra/datadog@1.4.18/observability/datadog/CHANGELOG.md)
#### Patch Changes

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

### [@mastra/deployer@1.79.0](https://github.com/mastra-ai/mastra/blob/@mastra/deployer@1.79.0/packages/deployer/CHANGELOG.md)
#### 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:

  ```ts
  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:

  ```ts
  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:

  ```ts
  await setupBrowserStream(app, { getToolset });
  ```

  After:

  ```ts
  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](https://github.com/mastra-ai/mastra/blob/@mastra/deployer-netlify@1.2.38/deployers/netlify/CHANGELOG.md)
#### 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](https://github.com/mastra-ai/mastra/blob/@mastra/docker@0.12.0/workspaces/docker/CHANGELOG.md)
#### 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)

  ```typescript
  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](https://github.com/mastra-ai/mastra/blob/@mastra/duckdb@1.15.0/stores/duckdb/CHANGELOG.md)
#### Minor Changes

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

  ```ts
  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)

  ```ts
  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)

  ```typescript
  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)

  ```typescript
  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)

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

### [@mastra/e2b@0.15.0](https://github.com/mastra-ai/mastra/blob/@mastra/e2b@0.15.0/workspaces/e2b/CHANGELOG.md)
#### 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)

  ```ts
  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](https://github.com/mastra-ai/mastra/blob/@mastra/elysia@0.1.19/server-adapters/elysia/CHANGELOG.md)
#### Patch Changes

- Fixed `/api/openapi.json` containing `$ref`s 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](https://github.com/mastra-ai/mastra/blob/@mastra/evals@1.10.8/packages/evals/CHANGELOG.md)
#### 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.

  ```ts
  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](https://github.com/mastra-ai/mastra/blob/@mastra/factory@0.24.0/mastracode/factory/CHANGELOG.md)
#### 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:

  ```bash
  # 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)

  ```ts
  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

Related Repositories