@mastra/core@1.72.0

n0-computer/iroh@mastra/core@1.72.0Sep 30, 2026by Patrycja-J

Upgrading?

AI Summary

This release enhances infrastructure stability with live channel resolvers and multi-worker background task safety. It adds robust recovery for agents/workflows, introduces Microsoft Teams integration, and upgrades observability capabilities.

Key Highlights

  • Live channel resolvers (no-redeploy channel connections) via `channels()`
  • Multi-worker safety for background tasks via persisted ownership leases
  • Stronger durability & recovery for agents/workflows including EventedAgent fixes
  • New Microsoft Teams channel + expanded Connect integrations
  • Observability upgrades: trace aggregation + richer filtering

Breaking Changes

  • `session.respondToToolApproval(...)` now requires `toolCallId`
  • `@mastra/playground-ui` removed/renamed multiple UI APIs (ScrollArea, PageHeader, ToolCall -> Activity, color token API changes)

New Features

  • `maxRetries` and `failurePolicy` options for observational memory
  • `detach()` method for `DurableAgent.observe()`
  • `CyberRefusalHandler` processor
  • `sandboxId` support in `WorkspaceSandbox`
  • `summary` flag for `listWorkflowRuns`

Full Release Notes

## Highlights

### Live channel resolvers (no-redeploy channel connections)
`Mastra({ channels })` now accepts a resolver function (via `channels()` in `@mastra/connect`) so channel connections added/removed on Mastra Platform appear on a running server without redeploy; use `mastra.resolveChannels()` to fetch the current provider map at runtime.

### Multi-worker safety for background tasks via persisted ownership leases
Background task execution is now lease-fenced (persisted `ownerId` + expiring lease) so multiple managers can share one storage without double-running tasks or allowing stale workers to overwrite results; storage adapters were updated to enforce `expectedOwnerId`/lease write conditions.

### Stronger durability & recovery for agents/workflows (including evented engine fixes)
Durable and evented agents/workflows get major correctness upgrades: safer crash recovery, preserved timeouts across serialization, proper tool/result processing & transcript transforms on durable runs, `savePerStep` honored, and EventedAgent is back on the built-in evented engine with recovery support.

### Thread streaming and PubSub retention improvements (faster reconnects, less replay)
`subscribeToThread({ withInitialHistory })` now emits a `thread-history` chunk first and avoids replaying completed runs on retained backends; plus `PubSub.trimTopic()` (implemented in `@mastra/redis-streams`) lets Mastra delete finished run entries to prevent retained thread topics from growing unbounded.

### Observability upgrades: trace aggregation + richer filtering
Storage adapters can now support `aggregateTraces()` (`trace-aggregate` capability) and `@mastra/server` adds `POST /observability/traces/aggregate` for grouped/time-bucketed measures; advanced trace queries also add `runId/sessionId/userId/organizationId` filters across supported stores.

### New Microsoft Teams channel + expanded Connect integrations
Introduced `@mastra/teams` (Microsoft Teams channel provider) and added 25 Microsoft Teams tools in `@mastra/connect`; `@mastra/connect` also gains multi-connection support (connection selection via `connection_name`) and now bundles Slack/Telegram/Discord channel dependencies.

### Breaking Changes
- `session.respondToToolApproval(...)` now requires `toolCallId` (id-less approval responses are rejected); related approval APIs now key gates per thread/run and update grant scoping.
- `@mastra/playground-ui` removed/renamed multiple UI APIs (notably `ScrollArea` props, `PageHeader` props, `ToolCall` → `Activity` component renames, and significant color token API changes).

## Changelog

### [@mastra/core@1.72.0](https://github.com/mastra-ai/mastra/blob/@mastra/core@1.72.0/packages/core/CHANGELOG.md)
#### Minor Changes

- Added `maxRetries` and `failurePolicy` options to the observational memory `observation` and `reflection` settings. When `failurePolicy` is `'continue'`, an Observer or Reflector model failure no longer ends the agent turn. (#24863)

  ```ts
  import { Memory } from '@mastra/memory';

  const memory = new Memory({
    options: {
      observationalMemory: {
        observation: { maxRetries: 2, failurePolicy: 'continue' },
        reflection: { maxRetries: 2, failurePolicy: 'continue' },
      },
    },
  });
  ```

- Added support for resolving channel providers dynamically. The `channels` option on the `Mastra` constructor now also accepts a resolver function, so channel connections added on the Mastra platform show up on a running server without a redeploy. (#25149)

  ```typescript
  import { Mastra } from '@mastra/core/mastra';
  import { channels } from '@mastra/connect';

  export const mastra = new Mastra({
    channels: await channels({ projectId: 'my-project' }),
  });
  ```

  A resolver exposes its webhook and OAuth routes up front through `getRoutes()`, and the providers themselves are resolved at runtime. Use the new `mastra.resolveChannels()` method to get the current provider map; `getChannelProviders()` keeps returning the latest resolved snapshot. Static `channels` records keep working unchanged.

- Fence background task execution with a persisted, expiring ownership lease. (#24841)

  Several managers can now share one storage without running the same task twice. A running task records which worker owns it and when that ownership lapses, recovery only reclaims a task whose lease has expired, and a worker that lost ownership can no longer save a result — so a stale run cannot overwrite the current owner's outcome.

  Configure the lease length with `leaseDurationMs`:

  ```ts
  const mastra = new Mastra({
    backgroundTasks: { enabled: true, leaseDurationMs: 30_000 },
  });
  ```

  `recoverStaleTasksOnStart` keeps its default of `true`, which is now safe because recovery is lease-fenced.

  Storage writes can additionally be made conditional on the current owner and lease:

  ```ts
  await storage.updateTask(
    taskId,
    { status: 'completed', result },
    { expectedStatus: 'running', expectedOwnerId: 'worker-1' },
  );
  ```

  The new write conditions are only enforced by storage adapters that understand them. On an older storage package the manager still runs, but the conditions are ignored and writes fall back to unfenced behaviour — so upgrade the storage package alongside `@mastra/core` to get the guarantee.

- Added an opt-in `summary` flag to `listWorkflowRuns`. When set, storage adapters may return only `{ status, timestamp }` for each run snapshot instead of the full snapshot. (#25135)

  ```ts
  const { runs } = await workflow.listWorkflowRuns({ perPage: 20, page: 0, summary: true });
  ```

- Storage adapters that support the new `trace-aggregate` capability can return aggregated trace results through `ObservabilityStorage.aggregateTraces()`, which executes a `TrustedTraceAggregatePlan` and returns a `TraceAggregateResponse`. The capability is declared alongside `trace-query`. Adapters that do not support it reject the call with a `MastraError` whose id is `OBSERVABILITY_STORAGE_AGGREGATE_TRACES_NOT_IMPLEMENTED`. (#25124)

  ```ts
  import { parseTraceAggregateRequest, planTraceAggregate } from '@mastra/core/storage';

  const plan = planTraceAggregate(
    parseTraceAggregateRequest({
      timeRange: { from: '2026-08-01T00:00:00Z', to: '2026-09-01T00:00:00Z' },
      measures: ['count'],
    }),
  );

  const response = await storage.aggregateTraces(plan);
  ```

- Added `detach()` to `DurableAgent.observe()` so an observer can stop watching a run without affecting it. Leaving the `for await` loop over `fullStream` early (`break`, `return`, or a thrown error) now also detaches, instead of keeping the subscription open for the life of the process. (#25048)

  ```ts
  const { output, detach } = await agent.observe(runId);
  request.signal.addEventListener('abort', detach, { once: true });
  if (request.signal.aborted) detach();

  for await (const chunk of output.fullStream) {
    send(chunk);
  }
  ```

  `cleanup()` is unchanged. It still removes the run and its cached events, so only the process that owns the run should call it.

  **Behavior change:** when `idleTimeoutMs` fires without an `isAlive` probe, the observer now detaches, and the run's registry entry and cached events are kept. Before, a quiet but still-running run (for example, one waiting on a slow tool) was cleaned up. If you rely on the idle timeout to clean up runs whose process crashed, pass `isAlive` so the run is only cleaned up when it reports the run is no longer running.

- Added an optional `sandboxId` to the `WorkspaceSandbox` interface so hosts can read a provider's physical, reattachable sandbox id and persist it for deterministic reattach. (#24004)

  ```ts
  const id = sandbox.sandboxId ?? sandbox.id; // physical id when the provider exposes one
  ```

- Added `CyberRefusalHandler`, a processor that retries once when an OpenAI or Anthropic cybersecurity safeguard refuses ordinary work partway through an agent run. These refusals are often false positives, and nudging the model to continue usually gets past them. A second refusal on the same step is treated as genuine. (#25172)

  - **OpenAI** refusals (`cyber_policy`, "This content was flagged for possible cybersecurity risk") fail the model call. Register the handler in `errorProcessors`, before `StreamErrorRetryProcessor`.
  - **Anthropic** refusals stop the response with a `cyber` classifier refusal (`content-filter` finish reason). Register the handler in `outputProcessors`; the refused step is rolled back before the retry.

  ```ts
  import { Agent } from '@mastra/core/agent';
  import { CyberRefusalHandler } from '@mastra/core/processors';

  const agent = new Agent({
    id: 'coder',
    name: 'Coder',
    instructions: 'You are a coding agent',
    model: 'openai/gpt-5.6-sol',
    errorProcessors: [new CyberRefusalHandler()],
    outputProcessors: [new CyberRefusalHandler()],
    maxProcessorRetries: 3,
  });
  ```

  Coding agents created with `createCodingAgent` include it in both lanes by default, along with a `maxProcessorRetries` budget of 3 — output-step retries have no implicit default, so without a budget the Anthropic retry would be treated as an abort.

- Added a `requireDescription` option to the workspace `execute_command` tool. When enabled, the tool asks the model for a short plain-language `description` of each command, listed before `command` in the tool schema, so UIs can show it instead of the raw command. The option is off by default and the tool schema is unchanged when it is off. (#25032)

  ```ts
  const workspace = new Workspace({
    sandbox,
    tools: {
      [WORKSPACE_TOOLS.SANDBOX.EXECUTE_COMMAND]: { requireDescription: true },
    },
  });
  ```

- Added `runId`, `sessionId`, `userId`, and `organizationId` filters to advanced trace queries. Use them at trace scope or inside `spans.some` / `spans.none` with `eq`, `ne`, `in`, `notIn`, `exists`, and `notExists`. Value discovery does not suggest these values, and `organizationId` can only narrow the trusted tenant scope. (#24935)

  ```ts
  const result = await observability.queryTraces(
    planTraceQuery(
      parseTraceQueryRequest({
        timeRange,
        where: {
          op: 'and',
          args: [
            { op: 'eq', left: { path: 'userId' }, right: { literal: 'user-42' } },
            { op: 'eq', left: { path: 'sessionId' }, right: { literal: 'session-9' } },
          ],
        },
      }),
    ),
  );
  ```

- Added `withInitialHistory` to `subscribeToThread`. The subscription first emits a single `thread-history` chunk with the stored thread messages, then only the retained parts that storage doesn't already cover, then live parts. Completed runs no longer replay on a retained backend such as Redis Streams, so chat UIs skip the replay animation and answered tool approvals aren't re-run. Pending approvals still come through so clients can prompt for them, and approvals already answered by a resumed run don't. Parts are compared to storage by when the model produced them, so slow publishing can't make stored parts appear again. (#24874)

  `AgentController` sessions now subscribe with `withInitialHistory`, so a session opened after a restart no longer re-acts on approvals from runs that already finished.

  The `thread-history` chunk type only appears on subscriptions that pass `withInitialHistory`; `agent.stream()` and plain subscriptions are typed exactly as before.

  ```ts
  const subscription = await agent.subscribeToThread({
    threadId,
    resourceId,
    withInitialHistory: { perPage: 40 },
  });

  for await (const chunk of subscription.stream) {
    if (chunk.type === 'thread-history') renderHistory(chunk.payload.messages);
    else applyLiveChunk(chunk);
  }
  ```

- Fixed scheduled workflows hanging when started directly on serverless hosts (#18807). Declaring `schedule` on a workflow no longer switches it to the evented engine unless the `MASTRA_WORKERS` environment variable is set. Dev servers, single-process servers, and serverless hosts keep the default engine, so an HTTP-triggered `run.start()` runs in-process like any other workflow instead of waiting for a worker that never starts. (#25430)

  **What changes**

  - Without `MASTRA_WORKERS`, scheduled workflows run on the default engine and no longer need storage with concurrent-update support. Cron fires run in-process on the host that runs the scheduler.
  - With `MASTRA_WORKERS` set to any value (split-worker deployments), scheduled workflows still run on the evented engine, and Mastra logs a warning to the console when one is created. Set `MASTRA_WORKERS` on every process in a split deployment, including `false` on the API.
  - `schedule` declared on Inngest workflows is ignored with a warning. Use Inngest's `cron` instead.

  ```typescript
  import { createWorkflow } from '@mastra/core/workflows';

  const dailyReport = createWorkflow({
    id: 'daily-report',
    schedule: { cron: '0 9 * * *' },
    // ...
  }).commit();

  // Before: dailyReport.engineType === 'evented', and run.start() hung without workers
  // After (MASTRA_WORKERS unset): dailyReport.engineType === 'default'
  const run = await dailyReport.createRun();
  await run.start({ inputData }); // completes in-process
  ```

  To keep a scheduled workflow on the evented engine everywhere, import `createWorkflow` from `@mastra/core/workflows/evented`.

- Added `PubSub.trimTopic(topic, { runId })` (no-op by default). Once a thread run completes successfully and its messages are saved, Mastra deletes that run's entries from the thread topic, so retained backends no longer keep finished runs. Entries are matched by `runId`, so a run resumed after a server restart also clears what it published before the restart. Runs still in progress, waiting for approval, or owned by another process are never touched. Threads whose agent has no storage are not trimmed. (#24875)

  Long runs are also trimmed as they go: each time messages are saved mid-run (by Observational Memory, or by `savePerStep` on `Agent` and `DurableAgent`), the parts up to the last finished step are dropped from the topic via the new `producedBefore` option. Pending approval and suspension prompts stay until the run itself is trimmed.

  Runs that fail, are canceled, or never start (for example a woken idle thread with no usable model credential) are deleted from the topic 30 seconds after they end. Nothing from them was saved, so reconnecting subscribers already ignore them; the delay lets subscribers reading live still see the failure.

#### Patch Changes

- Fixed cancellation during session startup so cancelled requests do not reach the model or interrupt a newer turn. Added the provider finish reason to AgentController error events so clients can distinguish token limits and refusals from execution failures. (#24321)

  Added a distinct session startup cancellation error so clients can handle interruptions without hiding provider or transport failures.

  ```ts
  import { isSessionStartupCancelledError } from '@mastra/core/agent-controller';

  try {
    await session.sendMessage({ content: 'Hello' });
  } catch (error) {
    if (!isSessionStartupCancelledError(error)) throw error;
    console.log('Interrupted');
  }
  ```

- Fixed cost estimates for Anthropic 1-hour prompt-cache writes. The 5-minute and 1-hour cache-write token counts are now read from the provider's raw usage, so 1-hour writes are priced at their real rate instead of the cheaper 5-minute rate. (#25011) (#25089)

  _Also in: @mastra/observability@1.18.2_

- Fixed `textStream` and `elementStream` returning incomplete array elements when streaming structured output with an array schema. Previously, if an element arrived across several tokens, the streams could keep an early partial version (for example `{"a":1}` instead of `{"a":1,"b":2}`) that didn't match the final `object`. Each element is now emitted only once it is complete, so the streamed elements always match the final result. (#25278)

- Update provider registry and model documentation with latest models and providers (`e1c3193`)

- Fixed tool approvals so they always go to the run that requested them. Approving a tool call after a newer run has started on the thread no longer resumes the wrong run. (#24874)

  `agent.sendToolApproval({ threadId, resourceId, runId, approved })` resumes exactly the run you name, including after a server restart. If that run has already ended, the call throws instead of resuming a different suspended run on the thread.

- Fixed `DurableAgent.recover()` failing with `Cannot read properties of undefined (reading 'messages')` or `(reading 'length')` when a default-engine run was killed while a tool was running or between steps. Running checkpoints now keep the conversation state a restart reads back, while still trimming older history to keep storage small. (#25107)

- Fixed channel messages being dropped when `chatOptions.concurrency` uses `burst`, `debounce`, or `queue`. Messages the Chat SDK batches together now reach the agent in the order they were sent and are saved to memory. Consecutive messages from one sender are merged into one turn; each sender's messages run as their own turn under that sender's identity. Custom channel handlers can read the batched messages from `context.skipped`. Fixes #22496. (#25168)

  ```ts
  import { Agent } from '@mastra/core/agent';
  import { createWhatsAppAdapter } from '@chat-adapter/whatsapp';

  export const agent = new Agent({
    id: 'support-agent',
    name: 'Support Agent',
    instructions: 'You are a helpful assistant.',
    model: 'openai/gpt-5-mini',
    channels: {
      adapters: { whatsapp: createWhatsAppAdapter() },
      chatOptions: { concurrency: 'debounce' },
      handlers: {
        onDirectMessage: async (thread, message, defaultHandler, context) => {
          console.log(`${context.skipped.length} earlier messages were batched`);
          await defaultHandler(thread, message);
        },
      },
    },
  });
  ```

- Fixed Channels tool approval cards so only the user who triggered the tool call can approve or decline it. In shared channels, clicks from other users are now ignored and logged instead of resuming the run. (#25165)

- Fixed an unhandled EPIPE error that crashed processes using UnixSocketPubSub when the broker process exited before stream teardown. Unsubscribing after the broker is gone now completes cleanly, and a publish that races `close()` now rejects with a clear "UnixSocketPubSub is closed" error instead of a raw socket error. (#25126)

- Fixed workspace `grep` and `list_files` failing with "No mount for path: .gitignore" when a `CompositeFilesystem` has no root (`/`) mount. Reads of unmounted paths now report a not-found (`ENOENT`) error, so a missing root `.gitignore` is treated as absent. (#25263)

- Fixed `restart()` and automatic recovery re-running a workflow step that had already finished. If the process stopped right after a step completed but before the next one started, restarting the run executed that step again, and for loops, foreach, parallel blocks, and branches that meant repeating every item, arm, or iteration. Restart now continues from the next step, and a run whose last step had already finished completes with that step's saved output instead of crashing. Agent-loop checkpoints now keep the just-finished step's conversation so the next step still receives it after a restart. Fixes #24615. (#25114)

  `DurableAgent.recover()` no longer hangs when the run stopped after the agent had already sent its final answer. The recovered stream now finishes with that saved answer, and `onFinish` runs once.

- Fixed a claimed thread owner running an idle wake twice when a sender retried the same cross-agent message after its acceptance acknowledgement was lost. Such a retry now resolves to the outcome the first attempt already accepted — its run, or the reason it failed or was cancelled before it ran — instead of starting a second turn that repeats the turn's tool side effects. (#24774)

- Fixed durable agents to reject invalid fallback timeout settings before preparation side effects. (#25188)

- Fixed tool result objects with a `value` key dropping sibling fields. (#25191)

- Fixed `DurableAgent` ignoring `savePerStep`. Each step is now saved to memory before the run continues, as with `Agent`, so a durable run that is interrupted mid-way keeps the steps it had already finished. (#24875)

  ```ts
  await durableAgent.stream('Research and summarize', {
    memory: { thread: 'thread-1', resource: 'user-1' },
    savePerStep: true,
  });
  ```

- Durable agents now honor the `awaited` background-task disposition by keeping the model turn open until the authoritative tool result is persisted. Replayed workflow steps adopt the matching persisted task, resume or restart it according to its status, and reconcile terminal results instead of dispatching the tool again. (#24569)

  _Also in: @mastra/inngest@1.10.1_

- Background tool results on the durable engine now apply configured transcript transforms, so payloads a user asked to redact are no longer persisted raw to thread history. They also use the configured Mastra `idGenerator` for appended message ids instead of falling back to random UUIDs. (#24569)

  _Also in: @mastra/inngest@1.10.1_

- Durable and evented agents now honor call-time `modelSettings.maxRetries`. Previously the durable llm-execution step's retry ladder only read retry counts from serialized model-list entries (hardcoding 0 for single-model agents), so a `maxRetries` passed via `stream()`/`generate()` model settings was silently ignored and failed requests were never retried. The agent-level retry config now rides on the serialized workflow options, and the ladder applies the same precedence as the in-process loop: an explicitly configured agent-level `maxRetries` (including 0) wins, otherwise the call-time `modelSettings.maxRetries` applies. (#24569)

- Data chunks emitted by output processors via `writer.custom()` are now persisted to thread history on the durable engine, matching the regular agent loop (#19375). Transient chunks remain stream-only. (#24569)

  _Also in: @mastra/inngest@1.10.1_

- Provider-executed tool results that arrive in a later stream (tool call in one step, result in the next) are now committed to the transcript on the durable engine instead of staying stuck in the call state (ports #14282 to the durable agent loop). Configured transcript transforms also apply to these deferred results instead of being silently skipped. (#24569)

  _Also in: @mastra/inngest@1.10.1_

- Fixed durable agents mishandling provider-executed tools (like Anthropic web search). Calls whose result had not arrived yet no longer fail with ToolNotFoundError, and results are no longer committed twice, which previously overwrote their provider metadata. (#24569)

  _Also in: @mastra/inngest@1.10.1_

- Fixed durable agents looping until maxSteps when a model response ended with a terminal finish reason (content-filter refusal or length truncation) alongside a tool call. The loop now stops instead of re-sending the same request and re-triggering the same refusal (ports #17893 to the durable agent loop). (#24569)

  _Also in: @mastra/inngest@1.10.1_

- Serialize `modelSettings.timeout` into the durable workflow input. `serializeModelSettings` dropped the `timeout` field entirely, so a durable run's execution time budgets were lost across the serialization boundary: cold recovery (`DurableAgent.recover()`) could not restore the run-level `totalMs` budget — a restarted run lost its configured deadline and ran unbounded — and the per-call `stepMs`/`firstChunkMs` budgets never reached the durable llm-execution step's shared execute wrapper. All three subfields now survive serialization (positive finite numbers only), and a recovered session re-arms the persisted `totalMs` budget. (#24569)

- Fixed `createDurableAgent` dropping non-transient `data-*` parts that tools write with `context.writer.custom()`. These parts are now saved to memory, just like with a regular `Agent`. Transient parts still only go to the stream. (#25283)

- Output processors' `processToolResult` hooks now run on the durable engine. Previously they were skipped entirely, so a redaction processor had no effect on durable streams or transcripts. Processor mutations and tripwire blocks now apply to the emitted chunk and the persisted transcript, before the raw value can reach subscribers. (#24569)

  _Also in: @mastra/inngest@1.10.1_

- The run-level `modelSettings.timeout.totalMs` budget is now enforced on durable agents (ports #21724 to the durable agent loop). Previously only the per-call budget applied, so a hanging provider or a long tool chain could run forever. The total budget now bounds the whole run, is re-armed with the original value on cold resume and recovery, and expiry surfaces as a run error rather than a silent stop. (#24569)

  _Also in: @mastra/inngest@1.10.1_

- Tool payload transforms now persist their state on the durable engine. Transcript-target transforms correctly redact stored args and results in thread history; previously only the streamed chunks were transformed and raw values were silently persisted. (#24569)

  _Also in: @mastra/inngest@1.10.1_

- Fixed DurableAgent dropping cached-input, cache-write, and reasoning token counts from `model_generation` span usage, so tracing exporters and cache/reasoning token metrics now match the regular Agent. (#25392)

- Durable runs of stored agents now stay pinned to the agent version they started on. Both resuming a suspended run (#22128) and recovering a crashed run re-resolve the original pinned version instead of silently switching to the latest published version. If the pinned version no longer resolves, the run falls back to the current definition with a warning. (#24569)

  _Also in: @mastra/inngest@1.10.1_

- Fixed saved dynamic workflows losing `.map()` entries that use `initData: true`. These mappings now keep reading from the workflow's initial input after the workflow is saved and loaded again, the same as they do in memory. Workflows saved before this fix need to be saved again. (#25152)

- Fixed `Session.sendMessage()` hanging forever when its run never produced a completion event, for example when several turns started at once on the same thread. It now rejects if the session's stream processing fails, and resolves if the run is aborted or the session's thread subscription is torn down. See #25140. (#25375)

- Fixed delegation bail() taking one extra model turn on the evented engine. When an onDelegationComplete hook calls ctx.bail(), the supervisor loop now stops in the same iteration on every engine. Previously the bail signal was passed between workflow steps by mutating a shared request context, which only works when all steps run in one process — on the evented engine each step gets its own copy, so the loop made one more model request before stopping. The bail signal now travels on the tool call step's serialized output, which crosses process and event boundaries reliably. The Inngest agentic loop's continuation predicate now also honors this flag, so bail() stops Inngest-hosted supervisor loops in the same iteration too (previously it only stopped via maxSteps). (#24569)

  _Also in: @mastra/inngest@1.10.1_

- Propagate the caller's actor identity through the evented workflow engine. Previously the evented engine accepted `actor` at the API surface (used for the fine-grained-authorization gate) but dropped it before execution: `execute()` params omitted `actor` and the workflow event payloads carried `requestContext` but not `actor`, so steps, tools, and loop conditions running on the evented engine never saw the caller's identity — actor-based authorization inside tools silently saw no actor. The actor signal now rides the event envelope alongside `requestContext` across every hop (start, resume, restart, time travel, step events) and reaches step, tool, agent, and condition execution contexts, matching the default engine. (#24569)

- EventedAgent runs on the built-in evented workflow engine again. It previously fell back to the default in-process engine because the evented path crashed on resume and recovery. Those bugs are fixed, and the engine is now covered by the durable-agent conformance suite — including crash recovery: a fresh process over the same storage can resume in-flight runs via `recover(runId)` / `recoverActiveRuns()`. (#24569)

- Fixed evented workflow snapshots growing quadratically with `.foreach()` input size. Each iteration's progress record no longer stores a copy of the whole input array, so large foreach runs no longer produce huge snapshots that can stall storage. Fixes #24943. (#25376)

- Durable agents no longer fail to start when their storage cannot apply concurrent workflow updates atomically. (#24569)

  The evented execution engine advances a run from concurrent workers, so it refuses to start on a storage adapter whose workflows domain does not support atomic concurrent updates. Durable agents now degrade to the default in-process engine with a warning in that case, the same way an agent with no Mastra host already did, rather than throwing on the first `.stream()` call. This keeps agents on adapters such as `@mastra/redis` and `@mastra/valkey` working, at the cost of evented execution — the warning names the adapter and the capability it is missing.

  A workflow that opts into the evented engine directly, by declaring a `schedule`, still gets an error, because there is no other engine it could have meant. That error now names the storage adapter it came from, names the missing capability, and lists the adapters that support it; it previously only suggested removing the `schedule` field.

- Fixed a redelivery race on the evented engine where a `workflow.step.run` event redelivered by an at-least-once transport after the step had already suspended would re-execute the step and overwrite the stored suspended record, stripping the suspendPayload that resume recovery depends on. The mid-step pre-write now drops a non-resume delivery when both the run and the target step record are suspended — a state in which no legitimate delivery exists — so the suspended record and its resume artifact survive redelivery. The check is a read-then-write that narrows the race window; fully closing it would need compare-and-set support on `updateWorkflowResults` across storage adapters. (#24569)

- Fixed evented workflows failing the entire run when a `.branch()` condition throws. A throwing condition is now logged and treated as false, so the remaining branches still run — matching the default workflow engine. (#25279)

- Fixed DurableAgent and EventedAgent treating falsy primitive resume payloads (`false`, `0`, `''`) as "no resume data" in the durable tool-call step. Suspended background tool tasks resumed with a falsy payload were stranded while a duplicate task was dispatched. The durable loop now uses the same nullish check as the regular agent loop (#22363), so only `undefined`/`null` mean "not resuming". (#24569)

- Fixed structured output on durable agents using the evented engine. Schemas passed as `structuredOutput.schema` now stay available on later model calls and after a run is recovered. Workflow options that cannot be saved now fail with an error naming the field. (#25075)

- Stored agents backed by FilesystemStore now retain durable execution settings. (#25123)

- Fixed `foreach` over a nested workflow failing with "already resumed by another caller" when one iteration suspended while others were still starting. Each iteration now starts its own child workflow run, and suspended iterations can be resumed individually with `forEachIndex`. (#25394)

- **Added atomic cross-process thread ownership handoff** (#24898)

  `agent.claimThreadOwnership()` now accepts `yieldOwnership`, `onOwnershipYielded`, and `onOwnershipLost` callbacks. When another process asks for a thread and `yieldOwnership` returns `true`, the thread passes to that process in the same request. `onOwnershipYielded` runs after the handoff. Owner lookups for message delivery never call `yieldOwnership`. If another process takes an expired claim first, `onOwnershipLost` runs so the caller can retry.

  `UnixSocketPubSub` now coordinates thread ownership between processes that share a socket directory. A running owner restores an expired claim when no other process has taken it. When an owner hands off a thread or exits, another process can take over immediately.

  **Before**

  ```ts
  const claim = await agent.claimThreadOwnership({
    resourceId,
    threadId,
  });
  ```

  **After**

  ```ts
  let claimActive = true;
  const claim = await agent.claimThreadOwnership({
    resourceId,
    threadId,
    yieldOwnership: () => session.thread.getId() !== threadId,
    onOwnershipYielded: () => {
      claimActive = false;
    },
    onOwnershipLost: () => scheduleClaimRetry(threadId),
  });
  ```

- Fixed durable agents crashing during crash recovery when the process was restarted while an LLM call was in flight. The active step now restarts from its own preserved input instead of a completed predecessor's pruned output, which previously caused "Cannot read properties of undefined (reading 'messages')" (#22636). (#24569)

- EventedAgent tool-approval, tool-denial, and in-execution resume no longer fail with "Cannot read properties of undefined (reading 'messages')": the evented engine retains running conversation history in persisted agent-loop snapshots, since its storage-merged step results are live data (unlike the default engine's write-only snapshots) (#24569)

  _Also in: @mastra/inngest@1.10.1_

- Fixed Gemini 3 rejecting the next request with "Corrupted tool call context" when native Google Search (`webSearchTool`) and another tool (such as a skill) were called in the same step. The provider-executed flag from the tool call is now kept when its result arrives without one, so the search call and its result stay together in the conversation history. (#24888)

- A paused goal now says why it paused. `/goal status` and the goal modal show the cause — evaluation budget exhausted, or a judge that failed to evaluate — and it survives a reload instead of disappearing after the moment it happened. Resuming or completing a goal clears the cause, so a later pause never shows a stale one. (#24618)

  _Also in: @mastra/code-sdk@1.9.0_

- Removed the unused `executeDurableToolCalls` helper and its `ToolExecutionContext`/`ToolExecutionError` types from `@mastra/core/agent/durable`. This code was never wired into any agent loop; both the durable and regular loops execute tools through their own step implementations. (#24569)

  _Also in: @mastra/inngest@1.10.1_

- Fixed `<PROVIDER>_BASE_URL` being ignored for providers such as Google, xAI, Perplexity, Cerebras, DeepInfra, Together AI, and Vercel. Setting `GOOGLE_BASE_URL` (or the equivalent for other providers) now routes requests and API keys through your proxy or gateway instead of the public provider host. (#25354)

- **Approval prompts are now independent per thread and run.** Concurrent and detached approvals stop overwriting, stranding, or answering each other's gates: gates are keyed by tool call and tagged with the thread and run that opened them, abort and user-message interjection release only the current thread's gates, detached-thread approvals no longer fire notifications or permission hooks, and an approved run resumes against the thread, run, resource, agent, and cancellation signal that actually parked it — so a mode switch, resource re-scope, or a successor run cannot redirect or cancel the parked continuation. (#24776)

  **Approval responses now require the gate's `toolCallId`.** Without the id, a response could release whichever gate happened to be parked — another thread's or another run's — so an id-less response is rejected instead of silently applied. Every caller inside the repo already passes the id; this only affects external callers of the session API.

  Before:

  ```ts
  session.respondToToolApproval({ decision: 'approve' });
  ```

  After:

  ```ts
  session.respondToToolApproval({ decision: 'approve', toolCallId });
  ```

  Similarly, `SessionApproval.arm()` now requires a `toolCallId`, and `getToolCallId()` is replaced by `getToolCallIds()` (which accepts an optional thread/run filter).

  **"Always allow" grants are now scoped to the gate's thread.** Approving with `always_allow_category` grants the tool's category to the thread that owns the gate rather than the whole session, so a background thread's approval cannot widen what the current thread may run without prompting. `grantCategory`, `grantTool`, `hasCategoryGrant`, `hasToolGrant`, and `getGrants` accept an optional `threadId`; passing one scopes the grant to that thread, omitting it keeps the existing session-wide behavior for embedders that grant explicitly.

  _Also in: @mastra/code-sdk@1.9.0_

- Fixed cross-agent wakes with `requireClaimedOwner` failing with "No claimed thread owner responded" when the owning agent was alive but slow to answer (for example on a busy host or CI). Owner discovery for wakes now retries with growing timeouts for up to 1 second before giving up, and the error states how long it waited. (#25488)

  Both limits can be tuned with environment variables:

  - `MASTRA_AGENT_THREAD_OWNER_DISCOVERY_TIMEOUT_MS` — per-attempt wait (default `100`)
  - `MASTRA_AGENT_THREAD_WAKE_OWNER_DISCOVERY_DEADLINE_MS` — total wake discovery budget (default `1000`)

- Fixed follow-up DeepSeek requests failing in thinking mode after switching providers or using tools. (#25189)

- Added an `outputEncoding` option to `LocalSandbox` so command output in non-UTF-8 encodings is decoded correctly. On Windows systems using a legacy code page, such as Chinese (GBK / code page 936), native commands previously returned garbled text. (#25266)

  ```ts
  const sandbox = new LocalSandbox({ outputEncoding: 'gbk' });
  ```

  UTF-8 remains the default. Fixes #25249.

- Fixed agents with working memory seeing the word "null" in their instructions when no working memory had been saved yet, for example on the first message of a new thread. The agent now sees "No working memory data available." instead. Fixes #23724. (#25057)

- Fixed `run.restart()` on the default workflow engine after a process crash during a nested workflow resume. Restart no longer fails with "This workflow run was not suspended", and the resumed step keeps its resume data and completes instead of suspending again. Fixes #25187. (#25234)

  Fixed `timeTravel()` ignoring resume values such as `false` or `0`.

- Evented workflows now persist the running step record and routing state before executing each step, so runs killed mid-step recover via restart() instead of failing with "Execution path is empty" (#24569)

  _Also in: @mastra/inngest@1.10.1_

- Fixed CachingPubSub dropping events that arrive from another caching tier. When a durable agent's stream cache follows a source bus that is itself a CachingPubSub with its own cache (for example a user-supplied CachingPubSub passed to new Mastra({ pubsub })), events from that tier already carry an index and were treated as local echoes and discarded — they never reached the agent's subscribers and could not be replayed. The follower now only treats indexed events as echoes when the source shares the same transport; foreign-indexed events are cached under a fresh index and republished so live delivery and replay both work across tiers. (#24569)

- Fixed durable agent resumes (including `InngestAgent`) running another agent's tool when two agents register tools with the same id. On a cold worker, tool calls now resolve against the run's own agent before falling back to Mastra-wide tools. (#25393)

- Deprecated Observational Memory `scope: 'resource'` in the memory config types. Editors now show the `scope` option as deprecated. Remove it to use the default thread scope, and enable `retrieval` or resource-scoped working memory for cross-thread continuity. (#24933)

- Fixed evented agent streams losing events when the agent uses a custom pubsub. The evented workflow engine publishes run events on the Mastra instance's pubsub, but the agent's stream listened only on its own pubsub — with a custom pubsub configured, suspend, finish, and tool events silently never reached the stream. The agent's caching pubsub now also follows the Mastra instance's pubsub as a source, so evented runs stream correctly regardless of which pubsub the agent was configured with. (#24569)

- Fixed tool approvals for Inngest durable agents (#25154). (#25167)

  - Runs created with `createInngestAgent()` that wait on tool approval now appear in `GET /api/agents/:agentId/suspended-runs` and `agent.listSuspendedRuns()`.
  - `approve-tool-call` and `decline-tool-call` now accept these runs. They no longer return "Access denied: durable run belongs to a different resource".
  - Durable runs waiting on an approval-gated tool now report `requiresApproval: true` and the tool's `args`.

  _Also in: @mastra/server@1.72.0_

- Fixed a race where a background tool result could be saved to memory without its configured transcript transform applied. When a background task finished while the turn was still being persisted, the memory history save could land last and store the raw tool result instead of the redacted one. All persistence paths now apply the same transcript redaction, so raw payloads never reach storage. (#24569)

- Fixed threads getting stuck on Anthropic and Bedrock after a step failed mid-reasoning. Prompts now leave out reasoning that the provider cannot accept, so later turns no longer fail. (#24928)

- Fixed model fallback logging so recovered failures no longer page as errors. When a model in a `models` chain fails and another model is still available, Mastra now logs a single warning naming the failed and next model instead of two error-level records. Failures of the last model in the chain are still reported as errors. (#25489)

- Processor retry feedback no longer invalidates the provider prompt cache. When a processor calls `abort(reason, { retry: true })`, the reason used to be added as a system message ahead of the conversation, so the retry and later steps missed the cache for the whole history. The reason is now added as a system reminder after the conversation, so each retry only adds to the previous request. (#25171)

  The reason is sent verbatim as `<system-reminder>{reason}</system-reminder>`. It is no longer wrapped in the `[Processor Feedback] Your previous response was not accepted: … Please try again with the feedback in mind.` template, so write the reason as the instruction you want the model to follow. The reminder is kept in thread history as a system-reminder signal, which default memory recall hides, like other processor reminders.

- Documented two `aggregateTraces()` result rules: null dimension values sort last in both sort directions, and a time range with no matching traces returns `rows: []` rather than a single zero-valued row. (#25016)

- Fixed `structuredOutput.jsonPromptInjection: false` being ignored when a structuring `model` is set. The main model's prompt no longer receives the full JSON schema on every step when you opt out of injection. (#25478)

- Fixed tool search loading every `search_tools` hit when `autoLoad` is `false`. Only `load_tool` now activates tools, so the tool list stays stable and the prompt cache is preserved across steps. (#25076)

- Fixed generated thread titles re-creating a thread that was deleted while the title was still being generated. Deleted threads now stay deleted, and the title save no longer overwrites thread metadata that changed during generation. (#25385)

- Fixed agent runs with `readOnly` memory still persisting their transcript when a background tool task completed. The background-result flush now honors the readOnly contract the same way the suspension flush already did. (#24569)

- Fixed durable and evented agents to reject invalid model timeout settings before starting a run. (#25079)

- Fixed thread subscribers showing a run as idle after a tool approval. When a run resumes, thread subscribers now get a `start` event for the resumed part. This applies to subscribers that were already listening and to those that join while the run resumes, so the UI shows the run as running again and can cancel it. (#24875)

- Fixed `createRunCommandTool` security checks on Windows. `allowedBasePaths` now accepts working directories with backslash paths (previously every subdirectory was rejected), command allow/block lists now recognize Windows paths such as `C:\tools\rm.exe`, and the unsafe-character filter now rejects dangerous input consistently on every call. (#25383)

- Added a `getBasePath` option to `AgentsMDInjector`. When set, relative tool paths resolve against that directory instead of `process.cwd()`, and `AGENTS.md`/`CLAUDE.md` files outside it are never injected. (#25071)

  ```ts
  new AgentsMDInjector({
    getBasePath: () => '/path/to/checkout',
  });
  ```

- Fixed Anthropic extended-thinking threads with working memory getting stuck on every turn with "thinking blocks in the latest assistant message cannot be modified". When the model spent a step only on an `updateWorkingMemory` call, the saved history kept that step's thinking without its tool call, so later requests replayed it merged into the next step. That step is now dropped when the call is hidden. (#24924)

  Threads already saved in this state recover when `ProviderHistoryCompat` is in the agent's error processors: on that Anthropic error it drops the leftover thinking step and retries once.

  ```ts
  import { Agent } from '@mastra/core/agent';
  import { ProviderHistoryCompat } from '@mastra/core/processors';

  const agent = new Agent({
    // ...
    errorProcessors: [new ProviderHistoryCompat()],
  });
  ```

  Fixes #22798.

- Fixed the durable agent engine leaking raw tool output when an output or result processor throws. Previously, if a processor (for example a redaction processor) failed with a non-tripwire error, the durable engine would warn and continue with the unprocessed value: the raw tool result was still emitted on the stream and persisted to the transcript. Both paths now fail closed — a throwing stream processor suppresses the chunk instead of emitting the original, and a throwing tool-result processor substitutes an error placeholder for both emission and persistence. The run itself stays alive in both cases. This matches the regular loop, which already refused to emit or persist raw values when a processor throws. (#24569)

- Fixed durable agents looping until maxSteps when a provider ends the stream with finishReason 'other' and no output. The empty response now surfaces as a stream error after one attempt, and completion checkers no longer grade errored iterations (ports #22273 to the durable agent loop). (#24569)

  _Also in: @mastra/inngest@1.10.1_

- Fixed subagents without memory failing immediately when their workspace has a browser. These subagents now complete their tasks and still see browser context in their prompt. (#25291)

- `submit_plan` results now record the reviewer's decision. `submittedPlan.action` is `approved` or `rejected`, and `submittedPlan.feedback` holds the reviewer's comments when they asked for changes. UIs can show the outcome from message history without parsing the result text. (#25413)

- Fixed semantic recall running messages from other conversations together on a single line. Each recalled message now appears on its own line in the system prompt, so the model can tell where one message ends and the next begins. (#25059)

- Fixed durable tool payload transforms after cross-process recovery. (#25387)

- Added the assistant message id to tool-input events so consumers can attribute streamed tool arguments to the model step that produced them. (#25196)

- Added `withInitialHistory` to thread subscriptions over HTTP. Pass it to `subscribeToThread` in `@mastra/client-js`, or to `useChat` in `@mastra/react`, to get stored thread messages as one `thread-history` chunk before live parts. Completed runs no longer replay on reconnect. (#24876)

  ```ts
  const subscription = await agent.subscribeToThread({ threadId, resourceId, withInitialHistory: { perPage: 40 } });
  ```

  ```tsx
  const chat = useChat({ agentId, threadId, resourceId, enableThreadSignals: true, withInitialHistory: true });
  ```

  Agent controller tool-approval requests now resume the stored suspended run when no approval is waiting in the session, so approval cards restored from thread history after a restart can be answered.

  _Also in: @mastra/client-js@1.51.0, @mastra/react@1.7.0, @mastra/server@1.72.0_

- Fixed DurableAgent streams to hide response metadata chunks and attribute finish chunks to the agent. (#25078)

- A function-form `toolDisplay` on a channel adapter can now render the approval card's final state after a user clicks Approve or Deny. Previously, Mastra always replaced a custom approval card with its own English "Approved" or "Denied" text, which also showed the tool's internal registry key. (#25486)

  The function now receives `approved` and `denied` events (`denied` includes `byUser`). Return `{ kind: 'post', message }` to replace the card:

  ```ts
  channels: {
    adapters: {
      slack: {
        adapter: createSlackAdapter(),
        toolDisplay: event => {
          if (event.kind === 'approved') return { kind: 'post', message: 'Onaylandı' };
          if (event.kind === 'denied') return { kind: 'post', message: `Reddedildi: ${event.byUser ?? ''}` };
          return undefined;
        },
      },
    },
  },
  ```

  If the function returns nothing, a blank message, or throws, the default card is shown.

  If your function switches exhaustively on `event.kind`, add cases for the two new kinds:

  ```ts
  case 'approved':
    return { kind: 'post', message: 'Approved' };
  case 'denied':
    return { kind: 'post', message: `Denied${event.byUser ? ` by ${event.byUser}` : ''}` };
  ```

- Fixed a TypeScript error when passing tools made with `createTool` to `new Mastra({ tools })` in projects that enable `exactOptionalPropertyTypes`. You no longer need a cast to register these tools. (#25064)

- Tool invocation parts now record `updatedAt` whenever their state changes (approval response, result, error), and `tool-call-approval` chunks carry the matching `updatedAt`. Compare them to tell whether a streamed approval is newer than the stored tool call. (#24874)

  ```ts
  for await (const chunk of subscription.stream) {
    if (chunk.type !== 'tool-call-approval') continue;
    const stored = storedParts.get(chunk.payload.toolCallId);
    if (stored?.updatedAt && chunk.payload.updatedAt <= stored.updatedAt) continue; // already stored
    promptForApproval(chunk);
  }
  ```

- Corrected the `ToolSearchProcessor` `search.minScore` documentation. Scores are raw BM25 relevance plus name-match boosts and can exceed 1, rather than falling in a 0-1 range. (#25498)

- The evented workflow engine now honors `emitStepEvents: false` (step-lifecycle watch events are suppressed instead of the option being silently ignored) and fails loudly when `createRun()` is called on an evented workflow that has no registered Mastra host, instead of starting a run that hangs or fails deep inside the event processor. Run-level events and execution routing are unaffected by the step-event gate. (#24569)

- `result.text` now keeps the answer when an output processor trips the wire. It matches `result.steps[].text`, `getFullOutput().text` and `result.response.messages`. Check `result.tripwire` to see whether the output was rejected. (#25483)

  ```ts
  const result = await agent.generate('...');
  if (result.tripwire) {
    // result.text still holds the rejected answer for logging or review
  }
  ```

- Fixed channel handler logs to preserve serialized error details and correlation. (#25349)

- Fixed the TypeScript type for DurableAgent stream options to include providerOptions. Provider-specific options (like OpenAI reasoning settings) were already forwarded at runtime, but TypeScript rejected them when passed to stream() or generate() on a durable agent. (#24569)

- Fixed suspended tool calls losing their suspension details when stored messages are converted to AI SDK v6 or v7 UI messages. `toAISdkMessages(messages, { version: 'v6' })` and `{ version: 'v7' }` now include the `data-tool-call-suspended` part (with the suspend payload and resume schema), matching the v5 output, so suspended tools stay resumable after a page reload. (#25134)

- Fixed `requestContextSchema` being silently ignored by durable and evented agents. `DurableAgent.stream()` (and evented subclasses) now validate the request context against the agent's `requestContextSchema` before starting execution, matching `Agent.stream()` and `Agent.generate()`. Invalid or missing context values now reject with the same validation error, instead of the run starting with unvalidated context. (#24569)

- Fixed evented agent streams hanging forever when a run fails before its first chunk. A workflow-level failure now surfaces as an error on the stream instead of leaving the consumer waiting. (#24569)

  _Also in: @mastra/inngest@1.10.1_

- Fixed AgentController not showing live progress for subagents delegated through `Agent.agents`. These subagents now appear in `displayState.activeSubagents` while they run, with their tool calls and text streaming in, instead of only showing up once they finish. Fixes #25019. (#25424)

- Fixed trajectory scorers in dataset experiments receiving an empty trajectory for workflow targets when no trace is stored. They now receive the workflow's executed steps, matching `runEvals`. (#25269)

### [@mastra/auth-workos@1.6.6](https://github.com/mastra-ai/mastra/blob/@mastra/auth-workos@1.6.6/auth/workos/CHANGELOG.md)
#### Patch Changes

- Fixed cookie sessions losing their organization on re-authentication. With `fetchMemberships: true`, a cookie session that has no selected organization now keeps the organization of the user's only membership. A selected session organization still takes precedence, and users with no memberships or several stay without an organization. (#25264)

### [@mastra/clickhouse@1.22.0](https://github.com/mastra-ai/mastra/blob/@mastra/clickhouse@1.22.0/stores/clickhouse/CHANGELOG.md)
#### Minor Changes

- Added `runId`, `sessionId`, `userId`, and `organizationId` filters to advanced trace queries at trace scope and inside `spans.some` / `spans.none`. No migration is needed. (#24935)

  _Also in: @mastra/duckdb@1.12.0, @mastra/pg@1.28.0_

#### Patch Changes

- Fixed the Studio Traces page running out of memory or timing out on large ClickHouse databases. Trace queries no longer read every stored trace's full data before applying the selected time range, which cuts memory use and data read on large tables. Fixes #25141. (#25225)

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

- Added a `summary` option to `workflow.runs()` to list runs without downloading full snapshots: `workflow.runs({ summary: true })`. (#25135)

#### Patch Changes

- Fixed workflow run methods failing with `404 Workflow run not found` when the run id contains characters like `+`, `&`, `#` or `/`. `start`, `resume`, `restart`, `timeTravel` (and their async/stream variants), `cancel`, `runById` and `deleteRunById` now URL-encode the run id, matching `createRun` and `startAsync`. (#25338)

### [@mastra/cloudflare@1.7.0](https://github.com/mastra-ai/mastra/blob/@mastra/cloudflare@1.7.0/stores/cloudflare/CHANGELOG.md)
#### Minor Changes

- Persist background task ownership so recovery can be fenced on the lease. (#24841)

  Adds `ownerId` and `leaseExpiresAt` to the background tasks schema and honours the new `expectedOwnerId` / `expectedLeaseExpiresAt` write conditions on `updateTask()`. Existing tables are migrated in place; rows written before the upgrade carry no ownership and are treated as reclaimable.

  Convex deployments must redeploy their schema and server functions: the new fields are declared as optional, but documents that carry them are rejected until the schema is pushed.

  _Also in: @mastra/cloudflare-d1@1.4.0, @mastra/convex@1.6.0, @mastra/dynamodb@1.4.0, @mastra/lance@1.4.0, @mastra/libsql@1.24.0, @mastra/mongodb@1.20.0, @mastra/mssql@1.10.0, @mastra/mysql@0.11.0, @mastra/pg@1.28.0, @mastra/spanner@1.9.0, @mastra/upstash@1.5.0_

### [@mastra/code-sdk@1.9.0](https://github.com/mastra-ai/mastra/blob/@mastra/code-sdk@1.9.0/mastracode/sdk/CHANGELOG.md)
#### Minor Changes

- Added `@mastra/code-sdk/schedules` with a process-local `ThreadScheduler` that sends recurring prompts into a thread on wall-clock boundaries, following daylight saving changes like cron. (#25476)

  ```ts
  scheduler.create(
    {
      prompt: 'Check whether the build is green',
      trigger: { kind: 'every', interval: { ms: 5 * 60_000, label: '5m' } },
    },
    { threadId, resourceId },
  );
  ```

  A schedule's source is a prompt, a prompt file re-read on each fire, or a script whose output becomes the prompt. `run()` reports whether a manual fire was delivered, failed, skipped because it was already firing, or not found. The Mastra Code controller exposes the scheduler as `threadScheduler`, and `createScheduleTools()` builds agent tools for it, enabled with the `scheduleTools` config option or the `signals.experimentalScheduleTools` setting (off by default).

- Added model, mode, and reasoning selection for Agent Client Protocol clients: (#24321)

  ```ts
  await connection.setSessionConfigOption({
    sessionId,
    configId: 'thought_level',
    value: 'high',
  });
  ```

  Fixed user prompts and internal system messages appearing as assistant replies. Failed turns now return errors that clients can display; token limits and refusals return their corresponding protocol stop reasons.

  Each conversation now has its own runtime and uses the client's requested working directory. Client-supplied stdio and HTTP MCP servers connect with their configured environment variables or headers. Unsupported legacy SSE servers return an error before session startup.

  Tool approvals and sandbox access requests now ask the client for permission. Cancelling a session stops its active turn and queued prompts without interrupting other conversations. Shutdown waits for cleanup even when multiple signals arrive.

  Headless runs report session cancellation during startup as aborted while preserving provider and transport errors. Session creation retains its original error even if cleanup also fails.

  Model lists now omit unconfigured providers and retain full routing IDs in labels, preventing gateway entries from appearing to be direct provider models.

  Added workspace skill discovery and invocation for Agent Client Protocol clients. Clients can list user-invokable skills and activate them with an ordinary prompt:

  ```ts
  await connection.prompt({
    sessionId,
    prompt: [{ type: 'text', text: '/skill/review Check the current changes' }],
  });
  ```

  Skills marked `user-invocable: false` are hidden and cannot be invoked through slash commands.

#### Patch Changes

- Mastra Code now retries once when OpenAI or Anthropic cybersecurity safeguards refuse ordinary coding work partway through a run, instead of stopping the run with a refusal error. (#25172)

  The behavior needs no configuration:

  ```ts
  import { mountAgentControllerOnMastra } from '@mastra/code-sdk';

  const { mastra, controller } = await mountAgentControllerOnMastra({ cwd: process.cwd() });
  ```

- Fixed cross-agent thread ownership so peers advertise the mastracode instance that currently owns the thread instead of an instance that only visited it earlier. (#24898)

  Sessions still keep every loaded thread advertised so saved peers remain reachable after `/new`. When another instance requests a thread that is no longer current, the SDK now transfers its lease during that claim attempt and forgets the yielded advertisement. A thread that is still current is retained, and retries succeed as soon as its current owner moves away. This prevents stale titles, missing peers, timeout races during event-loop stalls, and release-before-reclaim gaps.

- Fixed: mastracode no longer writes session scorer results into the local mastra.db. (#23632)

  The outcome and efficiency scorers run on every session and stored a result row each time, but nothing in mastracode ever read those rows back, so they only made the database grow. Scorer results are now discarded after each run instead of being kept on disk or in memory.

  **SDK change:** `createMastraCode().storage.getStore('scores')` now returns empty results from lookups and listings, including for scores already in the database. To read those older scores, open the database directly.

  Before:

  ```ts
  const { storage } = await createMastraCode();
  const scores = await storage.getStore('scores');
  const { scores: rows } = await scores!.listScoresByRunId({ runId, pagination: { page: 0, perPage: 50 } });
  ```

  After:

  ```ts
  import { LibSQLStore } from '@mastra/libsql';
  import { getDatabasePath } from '@mastra/code-sdk/utils/project';

  const store = new LibSQLStore({ id: 'mastra-code-scores', url: `file:${getDatabasePath()}` });
  const scores = await store.getStore('scores');
  const { scores: rows } = await scores!.listScoresByRunId({ runId, pagination: { page: 0, perPage: 50 } });
  ```

  Part of #22056.

- Fixed GitHub plugin installs failing when `package.json` declares pnpm with a Corepack integrity hash, such as `"packageManager": "pnpm@12.6.0+sha512.<hash>"` (the format written by `corepack use`). The hash is now accepted and passed to Corepack, which verifies the downloaded pnpm against it. (#25121)

- Quiet mode now shows a short description of each shell command, like `Drilling into the failed CI job`, instead of the raw command, so you can follow what the agent is doing without reading long commands and scripts. The agent is asked to write the description first and to phrase descriptions as a running narrative across commands. The description streams in as the agent writes it, and the raw command never flashes first. (#25032)

  Consecutive shell calls in the same directory share one compact box. When the quiet mode tool preview lines setting is 1 or more, the latest output streams into a shared preview at the top of the box:

  ```
  ╭──────────────────────────────────────────────────────────╮
  │  Test Files  3 passed (3)                                │
  │       Tests  42 passed (42)                              │
  ├──────────────────────────────────────────────────────────┤
  │ $ ~/code/my-project                                      │
  ├──────────────────────────────────────────────────────────┤
  │ ✓ Listing later commits touching the sandbox code  101ms │
  │ ✗ Checking the release tag                          1.5s │
  │   └▸ fatal: ambiguous argument 'v1.68.0..HEAD'           │
  │ ⠋ Running the sandbox test suite                      4s │
  ╰──────────────────────────────────────────────────────────╯
  ```

  Each directory gets its own box (subdirectories of the project show as `./path`), running commands show a spinner and a live timer, failed commands show their error line, and background commands are marked as started. With preview lines set to None, the box has no preview. Ctrl+E still reveals the full command and output.

- Added `mastracode prune`, which cleans up the local database from the shell instead of from inside the interactive session. (#23632)

  It deletes data older than the retention policies, and `--vacuum` returns the freed space to the operating system for a local libsql database (remote libsql and Postgres only delete rows). `--keep-memory` keeps chat history. This works even when the interactive session will not start, which previously left a large database with no way to reclaim it from inside the tool.

  ```bash
  mastracode prune                 # delete rows past the retention policies
  mastracode prune --vacuum        # ...then compact the files to reclaim disk
  mastracode prune --keep-memory   # ...but keep chat history
  ```

  `mastracode prune` and `/prune` refuse to run while another session is open, and a session started during either waits for it to finish. The lock lives in the app data directory, so sessions sharing one `MASTRA_DB_PATH` with different `MASTRA_APP_DATA_DIR` values don't see each other.

  Part of #22056.

- A routine goal save could delete a live goal. Saving used to double as deleting whenever no goal happened to be loaded in memory, so an empty ordinary save could remove the objective without explicit deletion intent. Saving now deletes only after an explicit clear; `/goal clear` still removes the goal and its older stored copy. If that delete fails, `/goal clear` now reports that the goal may still be active instead of claiming success, and switching back to the thread retries the delete rather than silently restoring the goal. (#24618)

  For anyone using `GoalManager` from `@mastra/code-sdk` directly: `clear()` followed by `saveToThread()` on the same thread still deletes the goal. The only change is that a save with no goal loaded, and no `clear()` before it, no longer deletes. `deleteFromThread` is also available to delete directly, and resolves to whether the delete landed:

  ```ts
  await goalManager.deleteFromThread(state);
  ```

- Fixed agents receiving the host app's `AGENTS.md` when working in a different repository (for example, Mastra Factory runs). Instruction-file reminders now resolve relative paths against the session's project path and only load instruction files from inside that checkout. (#25071)

### [@mastra/codemod@1.1.5](https://github.com/mastra-ai/mastra/blob/@mastra/codemod@1.1.5/packages/codemod/CHANGELOG.md)
#### Patch Changes

- Fixed the v1 evals codemod to migrate legacy scores imports. (#25108)

- Fixed v1 message type migrations to use valid agent and memory exports. (#25104)

- Fixed large codemod previews so printed and verbose output completes without hitting the process buffer limit. (#25244)

### [@mastra/connect@0.5.0](https://github.com/mastra-ai/mastra/blob/@mastra/connect@0.5.0/packages/connect/CHANGELOG.md)
#### Minor Changes

- Added multi-connection support and a string-array shorthand to `@mastra/connect`. (#24864)

  **Multi-connection support:** when a provider has more than one active connection, tools are wrapped with a required `connection_name` input and a new `<provider>__list_connections` tool is exposed so agents can discover and select which connection to use. Single-connection behavior and explicit `connectionId` pins are unchanged. The wrapper preserves each inner tool's approval contract (`requireApproval` and, when the MCP server-level policy is a function, `needsApprovalFn`) so approval prompts still fire for multi-connection MCP tools; missing or unknown `connection_name` fails closed and requires approval.

  The helper key uses a double underscore (`<integrationId>__list_connections`) so it cannot collide with a real provider tool of the form `<integrationId>_<toolName>` (for example, WorkOS ships a real `workos_list_connections` tool that lists SSO connections).

  Previously, multi-active provider connections were skipped with a warning.

  ```ts
  // connect() returns a resolver; call it to load tools.
  const tools = await connect({ integrations: ['linear'] })();

  // Agent lists available connections.
  await tools.linear__list_connections.execute({}, {});
  // => { connections: [
  //      { name: 'Work', accountLabel: 'Work' },
  //      { name: 'Personal', accountLabel: 'Personal' },
  //    ] }

  // Agent calls tools with the chosen connection.
  await tools.linear_get_issue.execute({ connection_name: 'Work', id: 'LIN-123' }, {});
  ```

  **String-array shorthand for `integrations`:** you can now pass a plain array of integration ids when no per-provider overrides are needed. The object form still works whenever you need `allowTools`, `disallowTools`, `autoApproveTools`, `connectionId`, or `disabled`.

  ```ts
  // Shorthand
  connect({ integrations: ['linear', 'github'] });

  // Object form (unchanged)
  connect({
    integrations: {
      linear: { allowTools: ['linear_get_issue'] },
      github: {},
    },
  });
  ```

  **`disallowTools` per provider:** each provider now accepts either `allowTools` or `disallowTools` — `ConnectIntegrationOptions` is a mutually exclusive union, so setting both is a compile-time and runtime error. Use `disallowTools` when you want the whole toolset minus a few keys instead of an explicit allowlist.

  ```ts
  connect({
    integrations: {
      // Everything except the delete tool
      linear: { disallowTools: ['linear_delete_issue'] },
      // Explicit allowlist still works
      github: { allowTools: ['github_get_repo'] },
    },
  });
  ```

- `channels()` now returns a live channel resolver instead of a fixed provider map. Channel connections created or removed on the Mastra platform are picked up by a running server automatically — no redeploy needed. (#25149)

  ```typescript
  import { Mastra } from '@mastra/core/mastra';
  import { channels } from '@mastra/connect';

  export const mastra = new Mastra({
    channels: await channels({ projectId: 'my-project' }),
  });
  ```

  The resolver keeps one provider instance per integration, refreshes platform connections on a configurable `ttlMs` cache (30 seconds by default), and re-applies credentials when a connection changes. Slack credentials are fetched fresh from the platform before each app-management call, so tokens refreshed by the platform are always honored.

  If you previously awaited `channels()` and read providers off the result as a plain object, call the resolver instead: `const providers = await resolver()`.

  Requires `@mastra/core` 1.72.0 or later — the first release whose `Mastra` constructor accepts a `ChannelsResolver` (the peer dependency range has been raised to match). Older cores treat the resolver as a static provider record and fail at construction.

- Added 25 Microsoft Teams tools. Areas covered: (#25361)

  - **Teams** — create, get, list joined teams, list members, add and remove members
  - **Channels** — create, get, list, update, delete
  - **Channel messages** — send, get, list, reply, list replies
  - **Channel tabs** — create, list
  - **Chats** — create, get, list, send messages, get message, list messages, list members

  One `microsoft-teams` connection powers both these tools and the Teams channel — no extra bot token or app registration to wire up.

  ```ts
  import { createMicrosoftTeamsTools } from '@mastra/connect';

  const tools = createMicrosoftTeamsTools({ connectionId: 'conn_...' });
  ```

- `@mastra/slack`, `@mastra/telegram`, and `@mastra/discord` are now direct dependencies of `@mastra/connect`. Installing `@mastra/connect` is enough to use any channel with `channels()` — no separate channel package installs required. (#25125)

#### Patch Changes

- Fixed Discord channel connections failing with `Discord rejected the bot token: 401: Unauthorized`. The Discord bot token is now read from the connection's metadata (`botToken`, following Nango's Discord convention) instead of the OAuth credential — Discord's OAuth exchange only yields a user Bearer token, which can never authenticate as a bot. A Discord connection without `botToken` metadata is skipped with a warning telling you to store the token on the connection. (#25260)

- Discord channel connections no longer require `applicationId` and `publicKey` on the platform connection. The bot token stored as the connection credential is enough — the provider resolves the rest from Discord automatically — so the "missing applicationId + publicKey" warning is gone. Connection metadata and `providerOptions` still work as explicit overrides. (#25125)

- Telegram channels now run in delegated credential mode: the provider receives a platform-backed `tokenResolver` instead of a static bot token, so a token re-pasted or rotated on the platform takes effect on the very next Bot API call without a snapshot refresh, and the bot token is never persisted in provider storage. (#25272)

  ```ts
  import { channels } from '@mastra/connect';

  // Telegram is wired automatically — the resolver reads the current
  // connection's api-key credential per Bot API call. No app-side config.
  const registrations = await channels({ projectId: process.env.MASTRA_PROJECT_ID! });
  ```

- Fixed Slack channel connections that failed with "Slack refresh token is invalid" when connecting an agent. The Mastra platform now manages the Slack credential refresh cycle, so `channels()` no longer competes with it over the single-use refresh token. (#25125)

### [@mastra/deployer@1.72.0](https://github.com/mastra-ai/mastra/blob/@mastra/deployer@1.72.0/packages/deployer/CHANGELOG.md)
#### Patch Changes

- Fixed workspace packages listed as externals being bundled or missing from deployable output. They now remain runtime dependencies packaged with the build. Builds also resolve dependencies consistently from an app or monorepo root. (#25110)

### [@mastra/discord@1.2.0](https://github.com/mastra-ai/mastra/blob/@mastra/discord@1.2.0/channels/discord/CHANGELOG.md)
#### Minor Changes

- A bot token is now enough to configure `DiscordProvider` — `applicationId` and `publicKey` are resolved automatically from Discord's `GET /applications/@me` when omitted, then persisted alongside the token. (#25125)

  ```typescript
  // Before: all three credentials were required
  new DiscordProvider({ app: { botToken, publicKey, applicationId } });

  // After: the bot token alone works
  new DiscordProvider({ app: { botToken } });
  ```

  Explicitly supplied values (config, `DISCORD_PUBLIC_KEY` / `DISCORD_APPLICATION_ID` env vars, or `configure()`) still take precedence over the resolved ones.

#### Patch Changes

- Switching the Discord bot token via `configure()` now replaces the app config instead of merging into it. Previously the prior application's `publicKey` and `applicationId` survived the switch — including in persisted config — so the old application's Ed25519 key kept verifying inbound webhooks while the new bot token was active. The provider now drops everything derived from the old token and re-resolves the new application's identity from `GET /applications/@me`. (#25149)

  For the same reason, `publicKey` and `applicationId` no longer fall back to `DISCORD_PUBLIC_KEY` / `DISCORD_APPLICATION_ID` when the bot token is supplied via config or `configure()` — stale environment values from a different application would otherwise attach to the new token. Environment fallback for those fields applies only when the bot token itself comes from `DISCORD_BOT_TOKEN`.

### [@mastra/docker@0.9.1](https://github.com/mastra-ai/mastra/blob/@mastra/docker@0.9.1/workspaces/docker/CHANGELOG.md)
#### Patch Changes

- Fixed `wait()` reporting a killed or timed-out process as a natural exit. Terminating a process returned `kill() === true` while the resolved `CommandResult` omitted `killed`, so a forced termination was indistinguishable from a command that exited on its own. (#24775)

  Killing a process tears its own exec stream down, so Docker can deliver the stream's `end` event while `kill()` is still confirming the process group is gone. `end` settled `wait()` first — without `killed`/`timedOut` — and the exit code it recorded then prevented `close`, the only path that carried that metadata, from settling.

  `end`, `close`, and `error` now settle through a single path, and `end` waits for an in-flight kill confirmation before settling. That wait is bounded, so a daemon that stops answering mid-kill cannot leave `wait()` pending; termination metadata is published by whichever event settles first.

- Failed `DockerTemplate` builds no longer leave a stopped intermediate container behind. (#25262)

### [@mastra/duckdb@1.12.0](https://github.com/mastra-ai/mastra/blob/@mastra/duckdb@1.12.0/stores/duckdb/CHANGELOG.md)
#### Patch Changes

- Fixed `listTraces`, `listTracesLight`, and `listBranches` scanning the entire `span_events` table when a query matched nothing or requested a page past the end. These calls now return an empty page immediately, so empty filters and out-of-range pages stay fast and use little memory on large stores. (#25271)

- Reduced storage use for ended spans in `@mastra/duckdb`. Span data returned by queries is unchanged. Fixes #25240. (#25268)

### [@mastra/e2b@0.12.2](https://github.com/mastra-ai/mastra/blob/@mastra/e2b@0.12.2/workspaces/e2b/CHANGELOG.md)
#### Patch Changes

- Fixed code mode failing after an E2B sandbox auto-paused on timeout. Code mode now resumes the paused sandbox and runs the program, instead of failing until another command happened to wake the sandbox. (#25261)

### [@mastra/editor@0.15.4](https://github.com/mastra-ai/mastra/blob/@mastra/editor@0.15.4/packages/editor/CHANGELOG.md)
#### Patch Changes

- Fixed stored processor graphs dropping `tools`, `activeTools`, and `toolChoice` after parallel or conditional steps, which caused the next model call to run without the agent's tools. (#25063)

- Fixed `@mastra/editor/composio` failing to import on fresh installs by upgrading `@composio/core` to ^0.18.0. Fixes #24512. (#25166)

### [@mastra/evals@1.10.4](https://github.com/mastra-ai/mastra/blob/@mastra/evals@1.10.4/packages/evals/CHANGELOG.md)
#### Patch Changes

- Fixed trajectory scoring so blacklisted and repeated tool calls inside nested agent and workflow steps now lower the score. Previously only top-level calls were checked. Fixes #24925. (#24929)

### [@mastra/factory@0.18.0](https://github.com/mastra-ai/mastra/blob/@mastra/factory@0.18.0/mastracode/factory/CHANGELOG.md)
#### Minor Changes

- Added the `factory_review_source` tool for Factory review-role sessions. It returns the Factory session URL that produced the review (the only field published on the PR/MR), the PR/MR author recorded at intake, the review card's own external source, and the intake-stamped repository identity. The review skills (`factory-review`, `factory-rereview`, `factory-gitlab-review`, `factory-gitlab-rereview`) now require calling this tool before publishing, cross-checking `triggeredBy` and `reviewTarget` against the PR/MR fetched at Phase 1 (with `boundRepository` as the binding-side repository identity when the card carries no URL), and including `sessionUrl` as a `Factory Session` block in the published body. This makes misattributed reviews (e.g. a review that lands on the wrong PR, or approves and requests changes at once) traceable back to the exact session that produced them, and blocks a wrong-target review before it publishes. (#25013)

  The tool takes no arguments and is only registered in review-role sessions where `MASTRACODE_PUBLIC_URL` is set to a non-blank browser-facing UI origin (a blank or whitespace-only value counts as unset, matching the Slack session-link surface). From an agent inside such a session:

  ```ts
  const { sessionUrl, triggeredBy, reviewTarget, boundRepository } = await tools.factory_review_source.execute({});
  // sessionUrl:      "https://factory.example.com/factories/<projectId>/workspaces/<sessionId>/threads/<threadId>"
  // triggeredBy:     "octocat" | null
  // reviewTarget:    { integrationId: "github", type: "pull-request", externalId: "github-pr:42", url: "https://github.com/acme/repo/pull/42" | null }
  // boundRepository: { provider: "github", repositoryId: 12345 } | { provider: "gitlab", host: "gitlab.example.com" | null, projectId: 101 } | null

  // Publish only `sessionUrl` on the PR/MR — `triggeredBy`, `reviewTarget`, and
  // `boundRepository` are inputs to the in-run cross-check and stay in the
  // session handoff so nothing on the review card's upstream (e.g. a Linear/Jira
  // issue slug) is leaked into a public review body:
  const publishedBlock = ['## Factory Session', `- Session: ${sessionUrl}`].join('\n');
  ```

  The skills instruct the agent to end every published review body with a `## Factory Session` section carrying `sessionUrl` verbatim, so a suspicious review can be traced back to the session that produced it.

#### Patch Changes

- Fixed Factory runs starting in the wrong repository when a project links several repositories. Automatic runs now stop when the target cannot be determined instead of choosing the first repository. (#25116)

- Improved Factory default model changes to ask whether running work and review sessions should switch too. New runs always use the saved default; existing sessions keep their current model unless the user explicitly switches them, and switched sessions adopt the new model on their next step. (#25337)

- Fixed Factory builds approved by someone other than the plan's owner starting in a new, empty session. The build now continues in the plan's session so it keeps the plan, and the planning agent stops once the build starts. Comments and edits on a GitHub issue no longer restart triage while its card is being built or reviewed (#25230). (#25270)

- Prevent client writes to internal reconciliation metadata (#24993)

- Added in-app GitHub and GitLab connections during Factory onboarding. Users can connect GitHub through the GitHub App install flow and connect GitLab without routing through Mastra Platform. The create-factory wizard now supports back navigation between steps. (#24855)

- Transcript rows share one line style. Tool calls, reasoning, signals, notifications, skills and the "Thinking" indicator show an icon and a label, with a chevron only when there is more to show. A state signal names its state and shows its mode as a badge. A row whose message fits on its line no longer offers a disclosure that only repeats the line, and a tool call with nothing to show, including one that returned `null` or an empty result, has no disclosure at all. (#24694)

- Board transitions and linked work item cards now apply while agent runs occupy every dispatch slot. Previously, closing an issue or opening a PR while `MASTRACODE_DISPATCH_MAX_IN_FLIGHT` runs were active left the card's stage stale until a run finished. Bookkeeping decisions now use their own small dispatch pool, so the board keeps mirroring GitHub while agents work. (#25289)

- Fixed Investigate on an Intake card not starting its triage session. The held triage run was marked finished without running because moving the card into Triage looked like the card had moved on from the run. (#25364)

- Fixed Factory kickoffs being delivered several times when they arrived while the previous run was ending. A kickoff queued onto an ending run is now resent only if it never showed up in the thread, and it is dropped instead of resent once the card has moved to another stage or its role was handed over. This stops a finished phase from restarting and pushing extra commits. (#25277)

- Review cards now show a merged pull request as merged as soon as the merge arrives, instead of offering Re-review until the next background sync. (#25369)

- Stopped comments and edits on a closed issue from queuing a new triage run on its finished card. (#25370)

- Pushes to a Factory-authored pull request now start a re-review automatically. Previously, once the review card reached Done, the re-review triggered by a new push waited for approval that was never requested, so someone had to click Re-review by hand. Pull requests from other authors still wait for approval as before. (#25371)

- Pending tool approvals are restored from stored messages when a conversation reloads, so approval cards survive a server restart without replaying finished runs. (#24876)

- Factory reviews now flag a visible misconfiguration warning when the review token is the PR author. Previously GitHub rejected the approve/request-changes submission and the verdict silently fell back to a plain PR comment. The verdict is still published as a PR comment (so the repair loop keeps working), with a warning placed after the verdict line explaining that a separate reviewer token is required for the verdict to count toward branch protection. (#25391)

- Factory reviews no longer publish a verdict on a pull request head that has already moved. Before posting, the review and re-review skills check that the PR head still matches the commit they verified. If a push landed mid-review, they review the new commits before publishing. (#25139)

- Fixed automated reviews failing with "Skill not found: factory-review". Review sessions started by the Factory now recognize their review role right away, so the review skills and the reviewer GitHub token are available from the first run. (#25341)

- Fixed automated pull request reviews failing with "Skill not found: factory-review." when the review session was opened before its review role was assigned. The run now picks up the review skills at kickoff instead of retrying until it gives up. (#25341)

- Fixed Factory cards looking finished while a review is still asking for changes. (#25377)

  - **Review cards:** a review pass now records its verdict on the card and leaves it in Reviewing. The card shows "Changes requested" or "Approved" with the reviewed commit. A merged pull request moves a Review card to Done, a pull request closed without merging moves it to Canceled, and the next push starts a re-review automatically.
  - **Work cards:** a Work card now moves from Building to Review when its pull request opens, shows the review verdict there, and moves to Done when the pull request merges. Agents can't move a Work card to Done while its pull request is still open or while the review requests changes.

- Fixed subagents in Factory sessions ignoring the project's default model. Explore, plan, and execute subagents now use the Factory default model instead of per-subagent models from the server's settings, which could name providers the Factory has no credentials for. (#25460)

  In Slack threads, subagents follow the sender's active model pack (explore uses the pack's fast model, plan uses plan, execute uses build), matching the main agent. When the sender has no pack they use the Factory default.

- Fixed Factory work sessions being able to load the review skills and approve their own pull requests. The factory-review, factory-rereview, factory-gitlab-review, and factory-gitlab-rereview skills are now only available to sessions running a Review phase (#25228). (#25273)

- Fixed Factory colors that lost contrast after the color role update: the sidebar Beta badge is brand green again and no longer clipped, invalid work item fields show a red border, project nodes in the knowledge graph have a visible ring, and the overview funnel shows people in pale purple again. (#25484)

- Fixed missing Slack feedback when Factory message preparation fails before dispatch. (#25362)

- New GitHub issues now create Factory work items when events are polled. Open issues filed after a repository was linked are recovered by the issue reconcile sweep when they have no work item. Changes to existing issues re-evaluate their linked work items. Events skipped during polling are logged at debug level. (#25201)

- Fixed session resume silently provisioning a replacement VM instead of reattaching to the original sandbox. Factory now persists the provider's physical sandbox id and forwards it back on resume, so providers that reattach by id (like Railway) reconnect to the same VM instead of orphaning it and doubling compute cost. Refs #23974. (#24004)

- Fixed pull requests opened through Factory so connected GitHub users are assigned to them, making their pull requests easier to find by assignee. (#25117)

- Blocked automatic Planning-to-Building transitions unless the project enables auto-approval or a person approves the plan. (#25329)

- Fixed new Slack threads starting chat-only sessions when they cannot be backed by a repository. (#24952)

  - Explain in Slack why a new Factory session cannot start when the linked project has no repository or source-control connection.
  - Keep account-link and project-selection prompts for senders who cannot yet be routed.
  - Keep chat-only sessions for deployments without account linking, projects, or source-control integration.
  - Leave existing Slack conversations unchanged.

- Fixed Factory sign-in on custom domains to explain that Mastra Platform authentication requires a Mastra-hosted domain and point to supported custom auth providers. (#25326)

- Fixed interrupted worker messages reporting successful delivery and recording a success audit. (#24321)

- Fixed the Label filter on the Factory Review and Work boards showing no values. Pull request, merge request, and Linear issue cards now carry their labels, so the boards can offer and filter by them. (#25095)

- Fixed Slack sessions using observational-memory models from an incompatible provider. New and restarted sessions now use a memory model compatible with their running model, including after a model switch fails. (#25412)

- Fixed replies from another linked Slack user using the responder's model credentials. Existing Factory Slack threads now keep using the session owner's provider credentials while preserving the responder's message attribution. Responders must belong to the session owner's organization, and subscribed follow-ups are rejected when the existing internal thread or owning session cannot be found. (#25474)

- Slack setup prompts now tell people to mention the bot again, or message it again in direct messages, after connecting their account or picking a default factory. (#25323)

- Added incident.io intake for self-managed servers: the generated Factory Server now wires the incident.io integration from INCIDENT_IO_API_KEY, and the incident.io status route reports the credential mode so clients can tell a deployment API key from Platform-managed connections. (#25178)

  ```ts
  import { MastraFactory } from '@mastra/factory';
  import { IncidentioIntegration } from '@mastra/factory/integrations/incidentio/integration';

  const factory = new MastraFactory({
    // ...
    integrations: [new IncidentioIntegration({ apiKey: process.env.INCIDENT_IO_API_KEY! })],
  });
  ```

- Fixed Slack default factory prompts so they appear as visible thread replies and notify the sender. (#25323)

### [@mastra/fastify@1.5.16](https://github.com/mastra-ai/mastra/blob/@mastra/fastify@1.5.16/server-adapters/fastify/CHANGELOG.md)
#### Patch Changes

- Fixed `@mastra/fastify` dropping response headers on streamed (`datastream-response`) routes. Auth cookies (`Set-Cookie`), redirects (`Location`), `Content-Type`, and custom headers now reach the client, so SSO login/callback, sign-in, sign-up, refresh, and logout work on Fastify. Multiple cookies are sent as separate headers, and headers set by plugins such as CORS are preserved. (#25280)

### [@mastra/inngest@1.10.1](https://github.com/mastra-ai/mastra/blob/@mastra/inngest@1.10.1/workflows/inngest/CHANGELOG.md)
#### Patch Changes

- Fixed `streamUntilIdle()` and `resumeStreamUntilIdle()` on `createInngestAgent()` agents running in the Mastra server process instead of on Inngest. The `/stream-until-idle` and `/resume-stream-until-idle` routes now run durably, the same as `stream(messages, { untilIdle: true })`. Fixes #25159. (#25190)

- Fixed recovery requests for agents created with `createInngestAgent()`. `POST /api/agents/:agentId/recover` now returns a clear 400 "not supported" error that points to `observe(runId)` for reconnecting to a running stream, instead of a 500. With `recovery.durableAgents: 'auto'`, startup recovery skips Inngest agents instead of logging an error for each one. Fixes #25160. (#25186)

- Fixed Inngest durable agents reporting `[object Object]` when a run fails. The stream error, `generate()` rejection, HTTP 500 response, and workflow tracing span now carry the real failure message, such as Inngest's `step output size is greater than the limit` error. Fixes #25161. (#25182)

- Fixed `InngestAgent.resume()` accepting runs that had already finished. Resuming a completed run now fails with a "not suspended" error instead of running the previously suspended tool again, so a declined tool approval can no longer be approved after the run ends. (#25181)

- Pass prior agent steps to per-step processor hooks in Inngest workflows. (#25217)

- Fixed resuming the second of several tool approvals from one agent turn by its `toolCallId` on Inngest durable agents. Approving the next pending tool call right after its approval request arrived used to fail with `no suspended tool call with id`. The resume now waits briefly for the new approval to be saved. Fixes #25158. (#25173)

- Inngest durable agents now tell Mastra which workflow name their runs are stored under, so pending tool approvals can be listed, approved, and declined over the HTTP API. Fixes #25154. (#25167)

- Fixed stopping an Inngest agent by thread or run id. Calling `abortThreadStream()` or `abortRunStream()` (including `POST /api/agents/:agentId/threads/abort` and the Studio stop button) now stops the run on the Inngest worker, and the stream ends with `finishReason: 'abort'`. Previously the call reported success while the run and its tools kept going. Fixes #25156. (#25192)

- Fixed Inngest durable agents ignoring `structuredOutput`. `generate()` now returns the parsed `object` instead of only the JSON `text`, and `stream()`, `resume()`, `resumeGenerate()`, and `observe()` in the same process also expose it. Fixes #25148. (#25170)

### [@mastra/langfuse@1.5.11](https://github.com/mastra-ai/mastra/blob/@mastra/langfuse@1.5.11/observability/langfuse/CHANGELOG.md)
#### Patch Changes

- Use the GenAI semantic convention for reasoning output tokens so Langfuse does not double-count them with total output tokens. (#24939)

- Fixed application version mapping on Langfuse traces and observations, and allowed overriding OpenTelemetry resource attributes. For example, with Langfuse credentials set in the environment: (#25101)

  ```ts
  import { LangfuseExporter } from '@mastra/langfuse';

  const exporter = new LangfuseExporter({
    resourceAttributes: { 'service.version': '2.3.1' },
  });
  ```

### [@mastra/libsql@1.24.0](https://github.com/mastra-ai/mastra/blob/@mastra/libsql@1.24.0/stores/libsql/CHANGELOG.md)
#### Patch Changes

- Fixed `$or` and `$nor` metadata filters treating the keys of one condition as alternatives. A filter like `{ $or: [{ category: 'electronics', inStock: true }, { name: 'novel' }] }` also matched out-of-stock electronics, and `deleteVectors` with such a filter deleted more vectors than intended. The keys of each condition are now combined with AND. (#25353)

  _Also in: @mastra/pg@1.28.0_

- Fixed schedules failing with "no such column: owner_type" on databases created by older versions. Existing schedules and trigger history are kept, and the upgrade is safe when several processes share the same database. (#25475)

- Workflow run lists requested with `summary: true` now read only status and timestamp from each snapshot, so large snapshots are not transferred from the database. (#25135)

  _Also in: @mastra/pg@1.28.0_

### [@mastra/livekit@0.4.1](https://github.com/mastra-ai/mastra/blob/@mastra/livekit@0.4.1/integrations/livekit/CHANGELOG.md)
#### Patch Changes

- Fixed workflow-driven voice turns being reported as successful when the workflow run failed. `createWorkflowReplyGenerator` now errors the reply stream when the run fails and no longer calls `onTurnComplete` for it, so a failed turn is no longer saved to memory as an empty or partial assistant reply. (#25068)

### [@mastra/loggers@1.3.3](https://github.com/mastra-ai/mastra/blob/@mastra/loggers@1.3.3/packages/loggers/CHANGELOG.md)
#### Patch Changes

- Fixed `HttpTransport` ignoring explicit `0` and `false` retry options. Setting `retryOptions: { maxRetries: 0 }` now makes a single request, `retryDelay: 0` retries immediately, and `exponentialBackoff: false` uses a constant delay between retries. (#25265)

- Fixed the Upstash logger so `maxListLength` actually trims the log list. Trim arguments were previously stored as log entries, causing unbounded list growth and unreadable entries in `listLogs()`. (#25496)

### [@mastra/mcp@2.1.1](https://github.com/mastra-ai/mastra/blob/@mastra/mcp@2.1.1/packages/mcp/CHANGELOG.md)
#### Patch Changes

- Fixed MCP tool calls failing with a schema validation error when tools are defined with zod v3 schemas. `MCPClient` also now accepts tools from other MCP servers that describe their inputs with JSON Schema draft 2019-09. (#25290)

### [@mastra/memory@1.33.0](https://github.com/mastra-ai/mastra/blob/@mastra/memory@1.33.0/packages/memory/CHANGELOG.md)
#### Minor Changes

- You can now control observational memory retries and choose whether a failed Observer or Reflector stops the agent turn. (#24863)

  - `maxRetries` sets how many times a failed Observer or Reflector call is retried. The default is `8`.
  - `failurePolicy: 'continue'` lets the agent turn finish when observation or reflection still fails after all retries. The default, `'abort'`, keeps the current behavior.
  - Messages that were not observed are retried on a later turn.
  - A cancelled turn always stops, whatever the policy.
  - Structured extractors now retry a temporary provider failure once. This retry does not use `maxRetries`, and a failed extraction still does not block the turn.

  ```ts
  import { Memory } from '@mastra/memory';

  const memory = new Memory({
    options: {
      observationalMemory: {
        observation: { maxRetries: 2, failurePolicy: 'continue' },
        reflection: { maxRetries: 2, failurePolicy: 'continue' },
      },
    },
  });
  ```

#### Patch Changes

- Fixed Anthropic extended-thinking threads with working memory getting stuck on every turn with "thinking blocks in the latest assistant message cannot be modified". Saving messages no longer keeps a thinking-only step behind after hiding its `updateWorkingMemory` call, and the hidden call no longer reappears in later prompts. Fixes #22798. (#24924)

- Fixed observational memory merging a later observation group into an earlier truncated group when the truncated group quoted an `<observation-group>` tag inline. The later group is now parsed and shown in reflections with its own ID and range, and stripping group tags keeps both the quoted text and the later observations. (#24927)

- Fixed Observational Memory leaving many remembered dates without a relative time such as "3 weeks ago". Dates with a time ("Mar 22, 2025 at 18:08"), ranges written with an en dash ("Aug 13–27, 2024"), month or year dates ("August 2024", "late 2023", "2035"), and date-range headers the reflector writes ("Date: Aug 1, 2024 - Feb 28, 2025") are now annotated. Dates written inside observations that include their year, such as "exam on January 10, 2024", are annotated too. Dates without a year are left unchanged. (#25179)

- Observational memory failure messages now include the Mastra error ID (for example `MASTRA_STORAGE_LIBSQL_UPDATE_BUFFERED_OBSERVATIONS_FAILED`), so storage failures show which operation failed instead of only the raw driver message. (#25366)

- Deprecated Observational Memory `scope: 'resource'`. Resource scope works much worse than thread scope for prompt caching and for the agent's understanding of the conversation. Using it now logs a one-time warning, and the option is marked `@deprecated` in types and docs. A new knowledge and subconscious memory primitive for cross-thread memory is coming soon and will replace resource scope. Until then, remove `scope` to use the default thread scope and enable `retrieval` or resource-scoped working memory for cross-thread continuity. (#24933)

  ```typescript
  // Before
  observationalMemory: { model: 'google/gemini-2.5-flash', scope: 'resource' }

  // After
  observationalMemory: { model: 'google/gemini-2.5-flash', retrieval: true }
  ```

- Fixed Observational Memory relative dates being off by a day when memory is read on a server in a different time zone from the one that wrote it. For example, an event from 8 days ago showed as "7 days ago". Fixed "1 week later" showing as "6 days later" across a daylight-saving change. A planned action dated for the current day is no longer marked as likely already happened. (#25177)

- Fixed `getSystemMessage()` returning working memory instructions that contained the word "null" when no working memory had been saved yet. It now shows "No working memory data available." instead. Fixes #23724. (#25057)

### [@mastra/modal@0.7.0](https://github.com/mastra-ai/mastra/blob/@mastra/modal@0.7.0/workspaces/modal/CHANGELOG.md)
#### Minor Changes

- Added Modal Volume mounts and a volume-backed workspace filesystem. (#25072)

  - New `volumes` option on `ModalSandbox` mounts Modal Volumes at the given paths.
  - New `sandbox.reloadVolumes()` picks up writes made to a Volume by other sandboxes.
  - New `ModalFilesystem` exposes a path inside the sandbox (such as a Volume mount) to workspace file tools, so agent files persist across sandbox restarts.

  ```typescript
  const sandbox = new ModalSandbox({ workingDirectory: '/workspace', volumes: { '/mnt/agent': volume } });
  const workspace = new Workspace({
    sandbox,
    filesystem: new ModalFilesystem({ sandbox, basePath: '/mnt/agent' }),
  });
  ```

### [@mastra/mongodb@1.20.0](https://github.com/mastra-ai/mastra/blob/@mastra/mongodb@1.20.0/stores/mongodb/CHANGELOG.md)
#### Patch Changes

- Fixed MongoDB skills listing ignoring the `status` and `entityIds` filters. `list({ status: 'published' })` no longer returns draft skills, `list({ entityIds: [] })` now returns no skills, and pagination totals reflect the filtered results. (#25133)

### [@mastra/mysql@0.11.0](https://github.com/mastra-ai/mastra/blob/@mastra/mysql@0.11.0/stores/mysql/CHANGELOG.md)
#### Patch Changes

- Fixed MySQL skills storage dropping a skill's inline file tree (`files`) on save, so skill files now persist and load correctly. (#25267)

### [@mastra/nestjs@0.2.31](https://github.com/mastra-ai/mastra/blob/@mastra/nestjs@0.2.31/server-adapters/nestjs/CHANGELOG.md)
#### Patch Changes

- Added support for NestJS v12. `@mastra/nestjs` now accepts `@nestjs/common` and `@nestjs/core` v12 as peer dependencies, so installing it in a NestJS v12 app no longer fails with a peer dependency conflict. Fixes #25077. (#25408)

- Fixed `MastraAuthGuard` failing to resolve when used on your own controllers. `MastraModule.register()` and `registerAsync()` now export `AuthService`, so you can protect app routes with Mastra's auth: (#25233)

  ```ts
  @Controller('api/things')
  @UseGuards(MastraAuthGuard)
  export class ThingsController {}
  ```

### [@mastra/observability@1.18.2](https://github.com/mastra-ai/mastra/blob/@mastra/observability@1.18.2/observability/mastra/CHANGELOG.md)
#### Patch Changes

- Fixed `model_step` spans exporting the provider's raw HTTP response body in their metadata. For some providers (for example, Azure OpenAI's Responses API) this added hundreds of kilobytes to each step span while repeating output and usage already on the span. Response headers are still exported, so provider request IDs and rate-limit details remain available for debugging. Fixes #24826. (#25347)

### [@mastra/pg@1.28.0](https://github.com/mastra-ai/mastra/blob/@mastra/pg@1.28.0/stores/pg/CHANGELOG.md)
#### Patch Changes

- Fixed duplicate observational memory records in PostgreSQL when several agents, Memory instances, or server processes start or reflect on the same thread or resource at the same time (#22188). Only one record is now created per generation, and every caller gets that same record back. This works behind transaction-mode connection poolers such as PgBouncer and Supabase. (#24923)

  No schema change or migration is needed. Databases that already contain duplicate records keep working: Mastra now always reads the same one (the earliest created) instead of switching between them.

- Fixed PgVector `$or` and `$nor` filters dropping field keys that sit next to a nested logical operator. In `{ $or: [{ $and: [{ a: 1 }], c: 2 }, { d: 3 }] }` the `c` condition was ignored. It is now combined with the nested `$and`. (#25353)

### [@mastra/platform-workspace@1.6.2](https://github.com/mastra-ai/mastra/blob/@mastra/platform-workspace@1.6.2/workspaces/platform-workspace/CHANGELOG.md)
#### Patch Changes

- Fixed nested folders in `PlatformFilesystem` workspaces so they open in Studio. Folders created by writing a file under them are now reported by `stat` and `exists`, and can be listed with `readdir`. (#25184)

### [@mastra/playground-ui@60.0.0](https://github.com/mastra-ai/mastra/blob/@mastra/playground-ui@60.0.0/packages/playground-ui/CHANGELOG.md)
#### Minor Changes

- Added three formatters so apps stop hand-rolling them: (#25286)

  - `formatBytes` from `@mastra/playground-ui/utils/number`: `formatBytes(1536)` returns `1.5 KB`.
  - `formatPercent` from `@mastra/playground-ui/utils/number`: `formatPercent(0.42)` returns `42%`, tiny ratios show `<0.1%`, and `{ signed: true }` returns `+12.3%` for changes.
  - `pluralize` from `@mastra/playground-ui/utils/string`: `pluralize(3, 'entry', 'entries')` returns `3 entries`.

- `ScrollArea` fade depth can now be set per side through `mask`. Fades stay 2rem deep by default; the main sidebar uses a 3rem top and 5rem bottom fade. A fade now grows with how far the content is scrolled from that edge, up to its depth, instead of appearing at full depth as soon as the content scrolls. (#25415)

  ```tsx
  <ScrollArea mask={{ top: '3rem', bottom: '5rem' }}>{items}</ScrollArea>
  ```

  **Breaking:** Removed the `viewPortClassName` and `viewportRef` props from `ScrollArea`. To style or reach the scrolling viewport, render the new `ScrollAreaViewport` as the direct child of `ScrollArea` and pass it `className` or `ref`. When you do not render it, `ScrollArea` adds it for you.

  ```tsx
  // Before
  <ScrollArea maxHeight="20rem" viewPortClassName="px-4" viewportRef={scrollRef}>
    {items}
  </ScrollArea>

  // After
  <ScrollArea maxHeight="20rem">
    <ScrollAreaViewport ref={scrollRef} className="px-4">
      {items}
    </ScrollAreaViewport>
  </ScrollArea>
  ```

  **Breaking:** Removed the deprecated `showMask` prop from `ScrollArea`. Use `mask` instead.

- `Dialog` now has one modern shell. Every dialog gets the same padding, spacing, close button, scroll fades and motion, so call sites only pass layout. (#25432)

  - **Sizes**: `<DialogContent size="sm" | "md" | "lg" | "xl" | "full">` sets the width: 24rem, 32rem (the default), 42rem, 56rem, or the full viewport. Height grows with the content up to a viewport cap.
  - **Body**: `DialogBody` is a padded ScrollArea whose edges fade while content scrolls. `layout="fill"` makes the body take the remaining height and lets its children handle scrolling, for example split panes or a pinned search. `flush` removes the inset.
  - **Footer actions**: `DialogCancel` closes the dialog. `DialogAction` is the primary action: `onConfirm` for a click, `confirmation="hold"` for press-and-hold, and now `type="submit"` to submit the surrounding form. Both are disabled while the dialog is `pending`.
  - **Root**: `intent="destructive"` now works on every dialog: it sets the `alertdialog` role, focuses Close first, and ignores outside clicks. A `<form>` placed directly inside `DialogContent` fits the layout without extra classes.
  - **Text**: titles use the `heading` role (16px), and descriptions use the `body` role, muted. `DialogDescription` is now visible.
  - **Motion**: dialogs scale up from 96% with a strong ease-out curve and close faster than they open. With reduced motion, they only fade.
  - **AlertDialog**: now built from the same shell and parts as `Dialog`, so it has the same padding, text roles, footer buttons and motion. Its header, body, footer, title, description and cancel are the `Dialog` parts. `AlertDialog.Content` takes the same `size` prop and defaults to `sm`. `AlertDialog.Body` is now the padded scroll area.

  ```tsx
  // Before
  <Dialog open={open} onOpenChange={isSaving ? undefined : onOpenChange}>
    <DialogContent className="max-w-2xl">
      <DialogHeader className="border-b border-border px-4 py-4">
        <DialogTitle>Import items</DialogTitle>
        <DialogDescription className="not-sr-only text-caption">Paste JSON or upload a file.</DialogDescription>
      </DialogHeader>
      <DialogBody className="max-h-[70vh] overflow-y-auto px-4 py-5">…</DialogBody>
      <DialogFooter className="px-4 pt-4">
        <Button icon={<X />} onClick={() => onOpenChange(false)}>Cancel</Button>
        <Button variant="primary" onClick={handleImport}>Import</Button>
      </DialogFooter>
    </DialogContent>
  </Dialog>

  // After
  <Dialog open={open} onOpenChange={onOpenChange} pending={isSaving}>
    <DialogContent size="lg">
      <DialogHeader>
        <DialogTitle>Import items</DialogTitle>
        <DialogDescription>Paste JSON or upload a file.</DialogDescription>
      </DialogHeader>
      <DialogBody>…</DialogBody>
      <DialogFooter>
        <DialogCancel>Cancel</DialogCancel>
        <DialogAction onConfirm={handleImport}>Import</DialogAction>
      </DialogFooter>
    </DialogContent>
  </Dialog>
  ```

  **Removed**

  - The `variant` prop on `Dialog` and the `DialogVariant` type. Every dialog now uses the modern shell.
  - `asChild` on `DialogTrigger`, `DialogClose` and `AlertDialog.Trigger`. Use `render` instead: `<DialogTrigger render={<Button>Open</Button>} />`.
  - `AlertDialog.Portal` and `AlertDialog.Overlay`. `AlertDialog.Content` renders both.
  - The `@mastra/playground-ui/lib/as-child` helper.
  - The `dialog-overlay-anim` and `dialog-content-anim` classes. To find the overlay in a test, query `[data-slot="dialog-overlay"]`.

- Added `FileDropBackdrop`, which wraps any field and shows a full-page "Drop to upload" empty state (customizable with `label` and `description`) while a file is dragged over the window. Dropped files matching the optional `accept` filter are passed to `onFilesDrop`. (#25299)

  ```tsx
  import { FileDropBackdrop } from '@mastra/playground-ui/components/FileDropBackdrop';

  <FileDropBackdrop accept="image/*,.pdf" onFilesDrop={files => setAttachments(prev => [...prev, ...files])}>
    <Composer>…</Composer>
  </FileDropBackdrop>;
  ```

- Added `PageHeader.Eyebrow` for a back link above the page title, and fixed `PageHeader.Meta beside` sitting below the title text when the header has a tall icon. (#25155)

  `PageHeader.Icon`, `PageHeader.Action`, and `PageHeader.Meta beside` now center on the first line of the title, including when the title wraps. A header with an icon or action contains them, so they no longer overflow the header, push the title up, or sit below it. Beside meta no longer shrinks when the title is long.

  ```tsx
  <PageHeader>
    <PageHeader.Eyebrow>
      <Link to="/alerts">
        <ArrowLeftIcon aria-hidden />
        Back to alerts
      </Link>
    </PageHeader.Eyebrow>
    <PageHeader.Title>Create alert</PageHeader.Title>
  </PageHeader>
  ```

  **Breaking:** removed the `title`, `description`, `icon`, and `isLoading` props from `PageHeader`. Compose the slots instead:

  ```tsx
  // Before
  <PageHeader title="Agents" description="Build and test agents." icon={<BotIcon />} />

  // After
  <PageHeader>
    <PageHeader.Icon>
      <BotIcon />
    </PageHeader.Icon>
    <PageHeader.Title>Agents</PageHeader.Title>
    <PageHeader.Description>Build and test agents.</PageHeader.Description>
  </PageHeader>
  ```

  For a loading state, pass `isLoading` to `PageHeader.Title` and `PageHeader.Description`.

- Keyboard focus now looks the same everywhere: a 1px neutral outline with no green halo. Any focusable element that does not style its own focus gets it by default. `focusRing` is a single class string instead of an object, with `focusRingInset` for clipped full-width rows and `focusRingOffset` for checkboxes, radios, switches, and filled `primary` and `destructive` buttons, where a line flush with the fill would disappear. Fields, buttons, segmented buttons, and clickable cards show focus by repainting their own rim to the same colour, so they keep their elevation; their focus edge is stronger than before, at the `--border-focus` weight tuned for 3:1 contrast. The halo, `ring` alias, and lighter rim focus tokens are removed. The Trace Intelligence empty state also drops its glowing and pulsing decorations. (#25441)

  Before:

  ```tsx
  <button className={cn('rounded-md', focusRing.visible)} />
  <div className="focus-visible:ring-1 focus-visible:ring-ring focus-visible:shadow-focus-ring" />
  ```

  After:

  ```tsx
  <button className={cn('rounded-md', focusRing)} />
  <div className={focusRingInset} />
  ```

  | Removed                                               | Use instead                           |
  | ----------------------------------------------------- | ------------------------------------- |
  | `focusRing.visible`, `.default`, `.simple`            | `focusRing`                           |
  | `FocusRingStyle` type                                 | none                                  |
  | `--ring`, `ring-ring`, `Colors.ring`                  | `--border-focus`, `ring-border-focus` |
  | `--shadow-focus-ring`, `shadow-focus-ring`            | none                                  |
  | `--focus-halo`, `Shadows['focus-ring']`               | none                                  |
  | `--surface-rim-focus`, `--field-rim-focus-on-surface` | `--border-focus`                      |

- Each thing an agent does in a chat now renders as its own `Activity` line: every tool call, reasoning step, signal, notification, skill and plain "working" row. A body is optional. Without one, the line has no chevron and otherwise looks the same, so a step that returns nothing reads like one that does. (#24694)

  ```tsx
  import { ActivityItem } from '@mastra/playground-ui/components/ai/activity';
  import { ToolCallOutput } from '@mastra/playground-ui/components/ai/tool-call';

  <ActivityItem icon={<Sparkles aria-hidden />} label="Thinking" status="running" aria-label="Thinking" />;

  <ActivityItem icon={<FileText aria-hidden />} label="Read file" detail="src/agent.ts" aria-label="Tool: view">
    <ToolCallOutput text={output} />
  </ActivityItem>;
  ```

  **The `ToolCall` shell is renamed to `Activity`**

  The compound parts moved to `components/ai/activity` under new names: `ToolCall*` becomes `Activity*`, `ToolCallPresentedHeader` becomes `ActivityHeadline`, and `ToolCallStatus` becomes `ActivityStatus`. `ActivityHeadline` takes its icon as an element instead of a component, and keeps the `description` prop that shows a command's description in place of its label and detail. Its `disclosure` prop is removed: pass `foldable={false}` to `Activity` instead. The tool-specific blocks stay in `components/ai/tool-call`: `ToolCallArguments`, `ToolCallOutput`, `ToolCallCommand`, `ToolCallGroup` and `presentTool`.

  ```tsx
  // Before
  import {
    ToolCall,
    ToolCallTrigger,
    ToolCallPresentedHeader,
    ToolCallContent,
  } from '@mastra/playground-ui/components/ai/tool-call';

  <ToolCall status={status}>
    <ToolCallTrigger>
      <ToolCallPresentedHeader icon={Search} label={label} detail={detail} />
    </ToolCallTrigger>
    <ToolCallContent>{body}</ToolCallContent>
  </ToolCall>;

  // After
  import {
    Activity,
    ActivityTrigger,
    ActivityHeadline,
    ActivityContent,
  } from '@mastra/playground-ui/components/ai/activity';

  <Activity status={status}>
    <ActivityTrigger>
      <ActivityHeadline icon={<Search aria-hidden />} label={label} detail={detail} />
    </ActivityTrigger>
    <ActivityContent>{body}</ActivityContent>
  </Activity>;
  ```

  The screen-reader status text is now "Running" or "Failed" instead of "Tool call running" or "Tool call failed", because the line is no longer only for tools.

  **Signals, notifications and reasoning are `Activity` presets**

  `components/ai/chat-event` is removed. Its presets moved into `components/ai/activity` and are named after the line they draw: `ChatSignal` is now `SignalActivity` and `ChatNotification` is now `NotificationActivity`. Each one picks the icon, badges and body for one kind of event over `ActivityItem`.

  The card presentation of signals and the notice presentation of notifications are removed along with their `variant` prop and `getNotificationNoticeVariant`. A notification's priority is now a coloured badge on the line: urgent is red, high is orange, medium is blue. Its status and pending count are badges beside it. A system reminder names its path as the detail of the line.

  ```tsx
  // Before
  import { ChatNotification } from '@mastra/playground-ui/components/ai/chat-event';

  <ChatNotification variant="notice" label="github / issue-opened" message={message} priority="high" />;

  // After
  import { NotificationActivity } from '@mastra/playground-ui/components/ai/activity';

  <NotificationActivity label="github / issue-opened" message={message} priority="high" />;
  ```

  `Reasoning` moved out of `domains/chat/messages/reasoning` and joins them as `ReasoningActivity`, with the same props. `hasVisibleReasoning` answers whether it would render anything, so a transcript can skip an empty reasoning part without drawing it. `ReasoningStreamingLine` is removed: `ReasoningActivity` covers the waiting state itself, and while it streams with no text yet, it shows a busy "Reasoning" line with no disclosure.

  ```tsx
  // Before
  import { Reasoning } from '@mastra/playground-ui/domains/chat/messages/reasoning';

  <Reasoning text={text} streaming />;

  // After
  import { ReasoningActivity } from '@mastra/playground-ui/components/ai/activity';

  <ReasoningActivity text={text} streaming />;
  ```

  **A line only folds when its body says more than the line**

  A short single-line message fits in the preview, so opening a disclosure used to reveal a copy of the line above it. Such a line now has no disclosure and wraps its detail instead of clipping it, so a narrow transcript never hides the end of a sentence it offers no way to open. Because folding is now the exception, a line that folds shows a dimmed chevron at rest instead of only on hover.

  Notification badges wrap in narrow transcripts without squeezing the message out. Linked notifications keep their full message in the expanded body. Expanded messages preserve line breaks and wrap long URLs. A notification link is now announced by its visible text followed by the message preview, for example "Open on GitHub: The pull request was merged…", so voice control can open it by what it says.

  The same rule covers a composed `Activity`: pass `foldable={false}` when there is nothing to open, and the line drops its disclosure button and its empty body. A tool call with no arguments, no output and no result is one example, and so is a call whose arguments are an empty object. `hasToolArguments` tells you whether `ToolCallArguments` would render anything, and `awaitsToolApproval` tells you whether `ToolApprovalButtons` would.

  ```tsx
  import {
    Activity,
    ActivityContent,
    ActivityHeadline,
    ActivityTrigger,
  } from '@mastra/playground-ui/components/ai/activity';
  import { hasToolArguments, ToolCallArguments, ToolCallOutput } from '@mastra/playground-ui/components/ai/tool-call';

  const foldable = hasToolArguments({ toolName, args }) || output !== undefined;

  <Activity foldable={foldable} status={status}>
    <ActivityTrigger>
      <ActivityHeadline icon={<Search aria-hidden />} label={label} detail={detail} />
    </ActivityTrigger>
    <ActivityContent>
      <ToolCallArguments toolName={toolName} args={args} />
      {output !== undefined && <ToolCallOutput text={output} />}
    </ActivityContent>
  </Activity>;
  ```

  A line that gains a body as its arguments stream in keeps its headline mounted: its shimmer does not restart, its detail does not fade in again, and the chevron fades into a slot that was already reserved, so the text does not shift sideways.

  Because the disclosure button now lies over the whole line, put anything that needs its own hover, such as a timestamp with a `title`, in `ActivityLeading`. It stays reachable above the button, so the tooltip still shows, and a click on it still opens the line. `ActivityHeadline` and `ToolCallGroup` already wrap their `leading` content in it. `ActivityTrigger` is now a wrapper around that button rather than the button itself, so props such as `onClick` or `disabled` no longer reach it: drive the line through `open`, `onOpenChange` and `foldable` on `Activity`.

  **`ChatTimeGap` is replaced by `TranscriptDivider`**

  The transcript separator is a `role="separator"` rule, not an event, and it no longer parses a time string. It takes the label and, separately, the timestamp that belongs in `title`, and renders nothing when the label is empty.

  ```tsx
  // Before
  import { ChatTimeGap } from '@mastra/playground-ui/components/ai/chat-event';

  <ChatTimeGap text="24 minutes later — Sep 17, 2026, 2:24 PM" />;

  // After
  import { TranscriptDivider } from '@mastra/playground-ui/components/ai/transcript-divider';

  <TranscriptDivider label="24 minutes later" title="Sep 17, 2026, 2:24 PM" />;
  ```

  `ChatSkill` is removed: the Factory was its only consumer, so it now composes `ActivityItem` itself.

  `SignalActivity` and `NotificationActivity` no longer set their own width or vertical margin, so the caller places them.

  **Studio draws workspace and memory steps as `Activity` lines too**

  Listing files, running a sandbox command and observational memory used to render their own cards, so a group of tool calls mixed two styles. They now use the same line as every other tool call. A listing shows its path on the line, and its summary and filesystem link beside it. A sandbox command shows its command on the line, and its sandbox link, exit status and duration beside it. The sandbox link is no longer nested inside the toggle button. A sandbox command now starts folded like any other call, unless it waits for approval. An observation or reflection shows its token counts on the line and keeps its observations, current task, suggested response and extractions in the body. A failed one is a failed line.

- Added a `switcher` slot to `Crumb` for entity switchers. On the current crumb, clicking anywhere on the crumb (name or chevron) now opens the switcher, instead of only the small chevron. On earlier crumbs the name still navigates back and the chevron opens the switcher. While t

Related Repositories