github mastra-ai/mastra @mastra/core@1.62.0
August 26, 2026

4 hours ago

Highlights

New storage backends: Elasticsearch + Valkey (GLIDE)

@mastra/elasticsearch adds an ElasticSearchStore that can power memory, workflow snapshots, scores, and semantic recall from a single Elasticsearch cluster, while new @mastra/valkey and @mastra/valkey-streams packages provide GLIDE-backed storage/cache plus PubSub/lease providers for Valkey.

Computer-use sandboxes (plus new E2B Desktop provider)

Workspaces can now expose an optional SandboxComputer capability (screenshots, mouse/keyboard control, display info, wait tools), with provider support landing in @mastra/daytona and a new @mastra/e2b-desktop package that bundles E2B command/process/filesystem + full desktop computer-use and authenticated noVNC.

Sandbox lifecycle + runtime environment control (less destructive shutdowns)

Sandboxes now own a runtime env (MastraSandbox({ env }), getEnv(), setEnv()), with @mastra/agentcore ensuring env updates reach commands across providers; shutdown behavior is safer too (Mastra.shutdown() stops instead of destroys remote sandboxes) and Workspace.stop() enables suspend/resume without teardown.

Observability + scoring upgrades (deterministic sampling, in-progress traces)

Scorer bindings gain declarative eligibility filters and deterministic sampling tied to the trace decision, improving consistency and ensuring scores only reference stored traces; on the observability side, PostgresStoreVNext now shows in-progress traces in Studio and span.endTree() enables force-closing a span tree so canceled work still exports complete traces.

Streaming & session UX improvements (SSE heartbeats + thread titling)

@mastra/ai-sdk exports withSseHeartbeat() to keep SSE streams alive outside chatRoute(), and @mastra/core adds AgentController#generateThreadTitle() for on-demand thread naming without spinning up a full session (propagating thread_title_updated through client streams and enabling “regenerate title” flows in Factory).

Breaking Changes

  • Mastra Code LSP is now opt-in ("lsp": true); by default it won’t start language servers or offer lsp_inspect
  • @mastra/playground-ui: MarkdownRenderer streaming no longer paces word-by-word reveal (use useRevealedText), and CSS class mastra-markdown-arriving renamed to mastra-arriving
  • @mastra/playground-ui: SankeySignals now requires controlled selectedFrameId and onFrameIdChange props
  • Background task storage is no longer exposed by Cloudflare KV or ClickHouse due to required atomic compare-and-set semantics
  • Removed exported types: BlaxelProcessManagerOptions (@mastra/blaxel) and RailwayProcessManagerOptions (@mastra/railway)
  • DaytonaSandbox command results changed: result.args removed and result.command now contains the full command string
  • agent.stream(..., { persistPartialOnAbort: true }) option removed; canceled runs now retain transcript history without opt-in

Changelog

@mastra/core@1.62.0

Minor Changes

  • Added optional collection row counts for application storage, so totals no longer require loading every matching row. (#22021)

    Before

    const total = (await storage.ops.findMany('jobs', { status: 'failed' })).length;

    After

    if (!storage.ops.count) throw new Error('Storage backend does not support counts');
    const total = await storage.ops.count('jobs', { status: 'failed' });
  • Added metadata filtering to score queries. You can now filter scores by metadata key-value pairs when listing scores: (#22047)

    const result = await storage.listScores({
      filters: { metadata: { env: 'prod' } },
    });
  • Added AgentController#generateThreadTitle() — name a thread on demand from where its conversation went, with the model and instructions generateTitle gives the first-turn namer. (#22156)

    It reads a bounded window of the thread's recent messages and writes the thread row without constructing a Session, so a "rename this conversation" action never spins up a workspace or sandbox for a session that is no longer live. A live session lends its agent, request context and event stream, and hears the resulting thread_title_updated.

    const title = await controller.generateThreadTitle({
      threadId,
      resourceId,
      // The caller's identity, so model resolution bills their credentials.
      requestContext,
      // Optional: hosts that store the title model themselves pass it here.
      model: ({ requestContext }) => resolveModel(storedModelId, { requestContext }),
    });
  • Sandboxes now own their runtime environment. MastraSandbox accepts an env constructor option, and you can read or update the environment at runtime with getEnv() and setEnv(updater): (#22250)

    sandbox.setEnv(env => ({ ...env, GH_TOKEN: token }));

    The sandbox environment is merged into every process spawn by the base SandboxProcessManager, so it reaches executeCommand() and processes.spawn() on any provider whose execution routes through its process manager, including values installed or rotated after the sandbox was created (for example, refreshed credentials). Per-call env options take precedence for that command only.

    These values apply to commands executed through the sandbox; they are not VM-level environment and are never written into the VM. WorkspaceSandbox declares setEnv as an optional capability.

  • Sandbox setup failures are no longer silent (#21984)

    An error thrown by an onStart handler now fails the start: start() rejects and the sandbox is marked error. Previously the error was logged and swallowed, so a sandbox whose setup never ran still looked healthy, and the failure surfaced later as a confusing command error.

    Added optional find(), connect() and create() for sandbox providers

    A provider can implement these three methods instead of writing start(). Mastra calls them in order, so providers no longer hand-roll "reuse the existing sandbox, otherwise make one", and Mastra knows which one happened. Existing providers that override start() keep working unchanged:

    class MySandbox extends MastraSandbox<MyHandle> {
      protected async find() {
        return (await sdk.list({ id: this.id }))[0];
      }
      protected async connect(handle: MyHandle) {
        this.vm = await sdk.resume(handle);
      }
      protected async create() {
        this.vm = await sdk.create({ id: this.id });
      }
    }

    Added the start outcome, so setup can tell a new sandbox from a resumed one

    onStart now receives outcome, either 'created' or 'connected', which is undefined for providers that don't report it:

    new MySandbox({
      onStart: async ({ outcome }) => {
        if (outcome === 'created') await installDependencies();
      },
    });

    Improved concurrent starts

    Two starts at once now share one attempt for every provider rather than only some, and a failed start is never cached, so the next call retries it. Starting a sandbox that implements neither start() nor create() throws instead of reporting itself running having provisioned nothing.

    Added setOnStart()

    For code handed a sandbox it didn't construct, this attaches a start hook after the fact. It takes an updater over the installed hook, so it composes with one already there instead of replacing it:

    sandbox.setOnStart?.(previous => async args => {
      await previous?.(args); // whatever set the sandbox up runs first
      await mySetup(args);
    });
  • Added an optional computer-use capability for workspace sandboxes. Providers can expose SandboxComputer to give agents screenshot, mouse, keyboard, screen information, and wait tools automatically when the workspace uses a static sandbox. Resolver-backed sandboxes do not register computer tools because their capabilities are unavailable when the tool list is constructed. (#21700)

    const workspace = new Workspace({ sandbox });
    
    if (supportsComputer(sandbox)) {
      const screenshot = await sandbox.computer.screenshot();
    }

    Computer action tools return a follow-up screenshot by default and support the existing workspace tool approval and enablement settings.

  • Mastra.shutdown() no longer destroys registered workspaces' sandboxes. Remote sandboxes (E2B, Platform, and other providers) were being killed on every process restart; they are now stopped instead, so providers that support suspension pause the sandbox and resume it later with filesystem and memory intact. (#22079)

    • New Workspace.stop(): shuts down language servers, closes the browser, and stops the sandbox without destroying anything. The workspace stays usable and the sandbox can start again later.
    • LocalSandbox.stop() now kills background processes (previously only destroy() did), so the stop-on-shutdown behavior cannot leak child processes on local sandboxes. Files in the working directory are untouched.
    • Full teardown remains explicit: workspace.destroy() or mastra.removeWorkspace(id, { destroy: true }).
    // Suspend live resources; the workspace stays usable
    await workspace.stop();
    
    // Full teardown is explicit
    await workspace.destroy();

Patch Changes

  • Fixed DurableAgent runs losing live models and tools while waiting on long-running tool or model calls. (#22141)

  • Fixed channel render failures crashing the host process before stream cleanup. (#22165)

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

  • Fixed nested workflow steps being omitted from foreach run details. (#22144)

  • Fixed the Mastra gateway so custom request headers can no longer replace the internal gateway authorization header (#21780). (#22134)

  • Route channel slash commands through AgentChannels handlers (#22223)

  • Removed obsolete generated capability files after registry refreshes. (#21531)

  • Fixed AI SDK reasoning-file and custom content parts being silently dropped from agent streams. Providers on the AI SDK v7 spec emit these parts, but Mastra discarded them when converting model output, so they never reached fullStream for either generate or stream calls. They now arrive as reasoning-file and custom chunks. (#22111)

    Content types Mastra does not recognize are no longer discarded either. They are now emitted as raw chunks, so you can see them by enabling raw chunks instead of losing the data with no error or warning:

    const stream = await agent.stream('Hello', { includeRawChunks: true });
    
    for await (const chunk of stream.fullStream) {
      if (chunk.type === 'raw') console.log(chunk.payload);
    }
  • Fixed missing observability spans for the processLLMRequest and processLLMResponse processor hooks. These hooks now emit processor_run spans like every other processor hook, so they appear in traces and in the automatic processor duration metrics, and tripwire aborts from them are recorded on the span. Fixes #22342. (#22351)

  • Fixed channel typing statuses staying pinned to the thread after a run ends without posting a message. When a run terminates on a tool call (for example via a stopWhen predicate), errors, or is aborted before any assistant text is posted, the typing status is now cleared instead of showing the last status indefinitely. Fixes #21880 (#22264)

  • Fixed durable agent runs writing the entire conversation into storage on every step. A long run persisted the accumulated message history and step output once per completed step, so the amount written grew with the square of the run length — a 57-step run produced hundreds of megabytes in Postgres and Redis, made saves slow enough to time out, and could fail an approval with a 500. (#22127)

    Historical completed steps in a still-running snapshot now keep only the fields the engine needs for routing, while the active step retains the conversation state needed for crash recovery. On a benchmark run this cut total bytes written by around 70% and peak snapshot size from 561 KB to 118 KB. Suspended and finished runs are stored exactly as before.

    Also fixed approvals that arrived while a large suspend was still being written. Resume read the run once and gave up if it did not yet look suspended; it now briefly re-reads snapshots that are still running or pending, while missing runs and statuses that cannot become suspended still fail immediately.

    Reported in #20747

  • Fixed suspended agent runs writing snapshots that grew quadratically with step count, which could exhaust memory on human-in-the-loop workflows using tool approval with large payloads. (#21732)

    Each buffered step of a run persisted its own full copy of the conversation so far — three times over, as model messages, database messages, and UI messages. A fifteen-step run was observed writing 22 MB of buffered steps over 2 MB of distinct messages, and production runs reached 170 MB per suspended run.

    Snapshots now record the IDs of the response messages referenced by each step and lazily rebuild those messages from the conversation that is already stored alongside them. This preserves the correct messages even when processors move or remove messages before suspension. Steps still expose the same messages after a run resumes, so no application code needs to change, while persisted snapshot size now grows linearly with step count.

    Fixes #17738.

  • Fix steps[i].content and steps[i].toolCalls being empty for input-step processors (processInputStep, prepareStep) and in the workflow output payload. Step content was extracted with a 0-indexed step count passed to a 1-indexed extractor and then sliced by message count, so the first step came back empty and later steps were misaligned. Completed steps now carry their real content, including tool results, which are re-read just before the next step's input processors run. (#22119)

  • Workspace no longer registers the lsp_inspect tool when LSP is not active, so agents are only offered the tool when it can actually run. (#22126)

    Also in: @mastra/code-sdk@1.5.0

  • Fixed cross-provider agent conversations leaking provider-hosted tool IDs into incompatible model requests. (#22148)

  • Fixed suspended agent runs resuming on a newer published version. When an agent is resolved from a status selector (for example { status: 'published' }), the version the run started on is now recorded when the run suspends and reused when it resumes, so approval and human-in-the-loop flows finish on the same version they started on. New runs still pick up the latest published version. Fixes #21846 (#22128)

    Also in: @mastra/ai-sdk@1.10.0

  • Fixed streaming channels posting blank messages when a response starts with whitespace-only text. (#22168)

  • Fixed session.thread.firstUserMessage() and firstUserMessages() returning nothing for agent-controller sessions. A live session persists a chat message as a user signal rather than a user row, and the lookup only matched the latter, so it came back empty for every real session. (#22156)

    Added isUserAuthoredMessage() for the same check anywhere else, and sessions now emit a thread_title_updated event when a generated title lands on the thread.

  • Preserve response message source tracking when processors return unchanged messages in multi-step agent runs (#22146)

  • Fixed Claude 4.6 and newer structured output requests that end with an assistant message, including agents without configured input processors. (#21925)

  • Improved workspace grep performance on remote filesystems by searching directories and files concurrently. (#22225)

  • Fixed semantic recall initialization to fail instead of silently selecting the default vector index when embedding dimensions cannot be determined. (#22171)

  • Fixed timeTravel() on a failed .foreach() step re-running iterations that had already succeeded, which duplicated their side effects (publishing, billing, uploads, notifications). (#22133)

    A failed foreach now records which iterations completed, so re-entering the step only runs the ones that did not.

    const run = await workflow.createRunAsync();
    await run.start({ inputData: { items: [1, 2] } }); // item 2 fails
    
    // Previously both items ran again. Now only item 2 runs.
    await run.timeTravel({ step: 'process-item' });

    Fixes #21749

  • Fixed slow streaming when a workflow is used as an output processor. The workflow runs once per streamed chunk, and each of those runs was saved to storage with the whole response so far, so a long reply got quadratically slower and heavier the more it streamed. Those per-chunk runs are now transient: they no longer write workflow snapshots or emit public traces, while the processor logic and tripwire behavior are unchanged. Running the same workflow directly still persists and traces as before. Fixes #19605. (#22124)

  • Made language-server (LSP) support opt-in in Mastra Code. By default Mastra Code no longer checks for LSP dependencies, starts language servers, or offers the lsp_inspect tool to the agent. Turn it on by adding "lsp": true (or an LSP config object) to your settings.json; "lsp": false is now also accepted and preserved. (#22126)

    Also in: @mastra/code-sdk@1.5.0

  • Fixed durable agent runs ignoring aborts requested before execution starts. (#22145)

  • Fixed workflow imports crashing in environments where the global fetch value is not callable. (#22246)

  • Fixed workspace search indexing so it can be rebuilt without starting the sandbox. (#22245)

    import { LocalFilesystem, Workspace } from '@mastra/core/workspace';
    
    const workspace = new Workspace({
      filesystem: new LocalFilesystem({ basePath: './workspace' }),
      bm25: true,
      autoIndexPaths: ['docs'],
    });
    
    await workspace.rebuildSearchIndex();
  • Fixed run.cancel() leaving a workflow's spans open when a step ignores abortSignal. (#22278)

    A step that never observes the abort signal keeps running, so the execution engine never unwinds and never ends the run's spans. Exporters that only act on span-end events (Langfuse, Mastra Cloud, and any OpenTelemetry-based exporter) therefore never received the run span or any of its ancestors, leaving traces without input/output, tags, or cost, and turning every step that did finish into an orphan. cancel() now closes the run's span tree itself, on both the default and evented engines.

    const run = await workflow.createRun();
    run.start({ inputData: {} });
    
    // The trace is now closed and exported even if the running step never returns
    await run.cancel();
  • Fixed malformed message parts crashing OpenAI-compatible providers when observational memory is enabled. (#22069)

  • Fixed EventedAgent leaving finished runs' snapshot rows in storage. Completed runs no longer show up in listActiveRuns() or get re-executed by recoverActiveRuns() (#22209). The evented workflow engine now also deletes a run's snapshot row when it reaches a non-paused terminal status that the workflow's shouldPersistSnapshot option declined to persist, instead of leaving a stale row behind. (#22266)

  • Fixed the onDelegationComplete hook so its result now includes finishReason (on both generate() and stream() paths) and its type declares subAgentToolResults. Hooks can now tell whether a sub-agent actually finished (finishReason: 'stop') or was cut off mid tool-call, and can read sub-agent tool results without casting. Fixes #21942. (#22311)

  • Add a /context command (alias /ctx) to Mastra Code that reports what is occupying the context window. (#22131)

    The report separates the startup context — system prompt, AGENTS.md/CLAUDE.md instructions with their source paths, the skills catalog, and MCP tool definitions rolled up per server — from context that accumulates during the session, namely the conversation itself and any observation memory injected into it. Each line carries an estimated token count and its share of the audited total, so it is possible to see which server, instruction file, or skill set is worth pruning.

    The audit reports only sizes and labels, never the audited content, and is printed to the terminal without being added to the conversation, so running it does not enlarge the context it describes.

    To support exact measurement, @mastra/core now exports formatSkillsCatalog, the pure formatter behind the skills processor's <available_skills> block, and @mastra/code-sdk exposes the assembled system prompt as labeled sections.

    Also in: @mastra/code-sdk@1.5.0

  • Experiment results now include isolated metadata snapshots from the dataset items that ran. (#22005)

    Also in: @mastra/client-js@1.42.1, @mastra/libsql@1.22.0, @mastra/mongodb@1.18.2, @mastra/mysql@0.8.2, @mastra/pg@1.22.0, @mastra/server@1.62.0, @mastra/spanner@1.6.3

  • Fixed background tasks restarting after reaching a terminal or suspended state. (#22140)

  • Fixed evented parallel and conditional workflows losing setState() updates from sibling branches; state changes from every branch are now merged into the workflow state (fixes #22319) (#22353)

  • Fixed background tasks failing to resume suspended sub-agent runs. (#22139)

  • Fixed nested workflows to preserve non-retryable failures across parent retry boundaries. (#22138)

  • Fixed concurrent agent.stream() calls on the same thread racing instead of serializing. A second stream() call on the same agent, thread, and resource now waits for the active run to finish before recalling memory, so each turn sees the previous turn's messages and message history is persisted in turn-complete order. Read-only runs and resumed/suspended runs are not affected. Fixes #21906 (#22312)

  • Fixed suspended run cleanup so cross-instance resumes keep their active thread lease. (#22150)

  • Purge deduped transient signals from MessageList source-tracking sets so removed messages no longer leave stale entries behind (#22107)

  • Stopped internal agent-loop workflows from logging a shouldPersistSnapshot excludes the "running" status warning on every resume. Workflows can now set options.allowUnclaimedResumes: true to acknowledge that resume claims cannot be persisted (because running snapshots are intentionally not written) and suppress the per-resume warning; the built-in agentic-loop, agentic-execution, and agent-network workflows opt in. User workflows that exclude running without acknowledging still get the warning. (#22262)

  • Prevent stale-build instances from executing scheduled workflow runs. (#22125)

    A scheduled fire carries only a workflow id, so whichever process consumes it resolves the step graph from its own registry. During a rollout a not-yet-cycled instance from the previous deploy could therefore run an outdated step graph — skipping steps the current build had added, including gates meant to stop the workflow from running at all — while HTTP runs of the same workflow on the same deployment executed the current graph (#19169).

    This is now fenced on both sides:

    • Declarative schedule rows record a hash of the workflow's serialized step graph. The scheduler refuses to claim a due fire when its local definition doesn't match the row, leaving the fire for an instance running the current build. If no instance claims it for several consecutive ticks, the scheduler escalates to an error and records a failed trigger rather than stalling silently — it never forces a stale fire through.
    • When the claiming process also runs workflow execution, the fire is published localOnly so the instance that verified the definition is the instance that runs it. Scheduler-only deployments, where pinning locally would strand the fire, keep publishing to the shared topic.
    • Fires that do reach the shared topic now carry the schedule's definition hash, and a consumer whose locally registered workflow hashes differently refuses the run and records a failed trigger instead of executing an outdated graph.

    Rows without a hash (legacy or imperatively created schedules) and agent schedules fail open and are unaffected.

  • Add declarative eligibility filters to scorer bindings, evaluated before sampling. (#22010)

    A scorer binding can now declare filter, a JSON-safe predicate over the scoring context (requestContext, entity, entityType, source, threadId, resourceId, projectId) using the shared predicate DSL. Filters run before sampling, so the sampling rate applies to qualifying traffic only — e.g. score escalated conversations at 100% by filtering on requestContext.escalated in one binding, and sample the rest at 5% in another.

    scorers: {
      groundedness: {
        scorer: groundednessScorer,
        filter: {
          op: 'eq',
          left: { path: 'requestContext.protocolVersion' },
          right: { literal: 'v3' },
        },
        sampling: { type: 'ratio', rate: 0.05 },
      },
    }

    Details:

    • Filters are validated at definition time: paths referencing unknown roots throw at agent construction (or when function-based scorer configs resolve), instead of silently skipping all scoring at runtime.
    • Filters evaluate against the flattened requestContext view that is persisted on score rows, so a filter remains answerable against stored records.
    • Filters are plain JSON and survive durable-agent serialization round-trips unchanged.
    • Scoring filters use the same predicate format as workflow conditional/loop conditions; existing workflow predicate behavior is unchanged.
    • Fixed: an unrecognized sampling.type previously fell through to scoring 100% of traffic; it now fails closed and skips scoring.
  • Make scorer sampling deterministic and tie it to the trace sampling decision (#22008)

    Ratio-based scorer sampling previously called Math.random() on every scorer invocation. It
    now hashes the trace ID (falling back to the run ID when observability is not configured), so
    the decision is reproducible and every scorer at a given rate selects the same traces. Two
    scorers sampling at 10% now cover the same 10% of traffic instead of overlapping on roughly 1%
    by chance, which makes their scores comparable on the same population.

    Scorers also now respect the tracing sampler. If the tracer declined a trace, scorers no
    longer run against it — those scores referenced a trace that was never stored and could not be
    drilled into.

    Two behavior changes when upgrading:

    • Score volume drops if your trace sampling rate is lower than your scorer sampling rate.
      The scores you lose are ones whose traces were never recorded. To keep the same score
      volume, raise your trace sampling to cover the traffic you score (or lower your scorer
      sampling to match — the effective rate is now bounded by the trace rate):

      // Before: scorer sampled 50% of invocations, but only 10% had a stored trace,
      // so up to 40% of scores referenced traces that were never recorded.
      new Observability({
        configs: { default: { sampling: { type: 'ratio', probability: 0.1 }, exporters } },
      });
      new Agent({
        scorers: { myScorer: { scorer: myScorer, sampling: { type: 'ratio', rate: 0.5 } } },
      });
      
      // After: raise trace sampling so every sampled score has a stored trace.
      new Observability({
        configs: { default: { sampling: { type: 'ratio', probability: 0.5 }, exporters } },
      });
      new Agent({
        scorers: { myScorer: { scorer: myScorer, sampling: { type: 'ratio', rate: 0.5 } } },
      });
    • Which traces get scored changes, even at an unchanged rate. Score counts before and
      after the upgrade are drawn from a different set of traces, not just a different number.

    Scoring is unaffected when observability is not configured: with no tracing set up there is no
    trace decision to inherit, so scorers run as before, now keyed deterministically on run ID.

  • Fixed experiments dropping a dataset item's expected trajectory. dataset.startExperiment and runExperiment now pass each item's expectedTrajectory to trajectory scorers, so a trajectory that matches the expectation scores correctly instead of always scoring 0. Inline experiment data can now supply expectedTrajectory too. Fixes #21743 (#22122)

  • Fixed cleared AgentController session preferences so they are removed from thread metadata and stay cleared after restart. (#22198)

  • Fixed recursive tool preparation for cyclic sub-agent graphs when background tasks are disabled. (#22244)

  • Fixed agent model calls to honor modelSettings.maxRetries when no retry count is explicitly configured on the agent or fallback model. Agents with no retry configuration continue to make a single attempt by default, while explicit agent or fallback settings—including maxRetries: 0—continue to override call-time settings. (#21947)

  • Fix Workspace tool output-validation errors reaching the model as untagged objects. sandboxToModelOutput now converts Mastra validation-error envelopes to AI SDK { type: 'error-json', value } tool results, so OpenAI-compatible providers serialize a tool message with valid content instead of rejecting the request. (#22306)

  • Fixed delegated (supervisor) resume dropping a sub-agent leaf tool's writer.custom() data frames from the parent stream. Resumed delegations now continue on the delegation thread persisted by the suspended run instead of generating a new one, so approval-gated tools that emit custom data frames keep streaming them after approval. Also, a failure to save a custom data frame to memory no longer removes it from the stream. Fixes #22217 (#22277)

  • Fixed transient signals duplicating within a turn. A processor that re-sends a transient reminder (e.g. via sendSignal with transient: true in processInputStep) now keeps a single fresh copy near the latest message instead of accumulating one copy per model call. Fixes #22060 (#22104)

  • Reduced the size of stored agent run snapshots. Completed steps of an agent run no longer keep a copy of the whole conversation, which previously made snapshots grow with every step of every turn and could push long conversations past a storage provider's document size limit. Resuming a suspended run, tool approvals, and run recovery are unchanged. (#22123)

  • Improved agent turn latency by reusing run-scoped memory reads (#22076)

    Also in: @mastra/memory@1.28.0

  • Documented that server.middleware handlers use Hono's signature and which serving paths run them. (#22161)

  • Fixed built-in memory processors so canceled agent runs retain transcript history through normal output processing. Removed the accidental partial-abort persistence option because cancellation persistence is no longer opt-in. (#22243)

    // Before
    await agent.stream('Hello', { abortSignal, persistPartialOnAbort: true });
    
    // Now
    await agent.stream('Hello', { abortSignal });
  • Fixed Agent.listSuspendedRuns() and DurableAgent.listActiveRuns() ignoring the resourceId filter at the storage level. The filter is now pushed down to the workflow storage query instead of being applied in JavaScript after fetching every snapshot in the date range, so scoped polling no longer deserializes unrelated runs. Durable and evented agents now also record the resourceId on their workflow runs when they are created. See #21844 (#22105)

  • Cancelling a durable agent run that is already executing now works through the agent APIs. agent.abortThreadStream({ resourceId, threadId }) and agent.abortRunStream(runId) only ever reached the abort controller a regular run prepares, which a durable run does not have, so they recorded a cancellation nothing acted on while the run streamed on. The server route POST /agents/:agentId/threads/abort goes through the same call. Both now publish the durable abort request as well, the same one the abort() on a stream result publishes, so the run stops in whichever process is executing it. (#22255)

    const { runId } = await durableAgent.stream('...', { memory: { thread, resource } });
    
    // before: the cancellation was recorded and the run streamed on to completion
    // after: the run stops and onAbort fires
    durableAgent.abortRunStream(runId);
  • Added a denied flag to the agent controller tool_end event. Approval-denied tool calls and tools aborted while parked at an approval gate already emit tool_end with isError: false (the tool didn't fail — it never ran), which made them indistinguishable from a real successful completion. Subscribers that need to know whether the tool actually did work can now gate on denied !== true. (#22213)

    session.subscribe(event => {
      if (event.type !== 'tool_end') return;
      if (event.isError) return; // tool ran and failed
      if (event.denied) return; // tool was approval-denied or aborted before it ran
      // ...tool actually executed successfully
    });
  • Fixed agent skill search to rank relevant instructions and references instead of relying on literal substring matches. (#22174)

  • Fixed resumed sub-agent delegations failing thread-ownership validation. The resume path backfilled the thread from the suspended run's snapshot but still passed a freshly generated resource ID, so resuming a delegated run threw "A thread can only be used by the resource that owns it". Both the thread and resource are now restored from the snapshot on resume. Also fixed model stream transport handles (e.g. WebSocket routing) being dropped when modelSettings.timeout wraps the model stream. (#22287)

  • Prevent background task dispatch from overwriting concurrent cancellation (#22228)

  • Enforce atomic conditional background task state updates so cancellation cannot be overwritten during dispatch. Background task storage is no longer exposed by Cloudflare KV or ClickHouse, which cannot provide the required compare-and-set semantics. (#22228)

    Also in: @mastra/clickhouse@1.16.0, @mastra/cloudflare@1.6.3, @mastra/cloudflare-d1@1.3.1, @mastra/convex@1.5.5, @mastra/dynamodb@1.3.2, @mastra/lance@1.3.1, @mastra/libsql@1.22.0, @mastra/mongodb@1.18.2, @mastra/mssql@1.7.3, @mastra/mysql@0.8.2, @mastra/pg@1.22.0, @mastra/spanner@1.6.3, @mastra/upstash@1.4.3

  • Fixed grouped channel streams closing between agent steps. (#22173)

  • Fixed Anthropic 400 errors ("thinking or redacted_thinking blocks in the latest assistant message cannot be modified") during tool-use turns with extended thinking. ProviderHistoryCompat no longer strips reasoning parts from the trailing assistant message of an active tool-use continuation, which Anthropic requires to be replayed exactly as it was generated. Historical assistant messages are still sanitized. (#22099)

  • Fixed an infinite agent loop when a provider stream ends with finish reason "other" without producing any output. The agentic loop previously re-issued the identical request until maxSteps was reached, silently burning tokens. Such zero-output streams are now treated as a stream error so error processors can retry a bounded number of times before failing loudly. Streams that finish with reason "other" after producing output continue as before. Fixes #21897 (#22273)

@mastra/agentcore@0.4.1

Patch Changes

  • Honor the sandbox runtime environment (setEnv()/getEnv() from @mastra/core) in every workspace sandbox provider. Environment variables set after construction now reach subsequent commands on all providers: (#22250)

    • Process-manager-routed providers (E2B, Blaxel, Cloudflare, Daytona, Docker, Modal, Vercel microVM spawns) inherit the merge from the core spawn wrapper; their duplicated per-manager env plumbing is removed.
    • Providers with their own exec transports (AgentCore, Apple Container, Railway, Vercel microVM and serverless executeCommand, Platform's private-network, WebSocket lease, and E2B lease paths) now merge getEnv() under per-call env.

    Constructor env continues to behave as before: it seeds the sandbox runtime environment, and providers that bake env into the VM or container at creation time (Docker, Modal, Railway, Apple Container, Vercel, Platform) still do so. Per-call env on executeCommand still takes precedence for that command only.

    Removed exported types (minor bump for these two packages): BlaxelProcessManagerOptions from @mastra/blaxel and RailwayProcessManagerOptions from @mastra/railway. Both existed only to pass env into a process manager constructed by hand; the core spawn wrapper now owns that merge, so the option and its type are gone.

    Also in: @mastra/apple-container@0.4.1, @mastra/blaxel@0.8.0, @mastra/cloudflare-sandbox@0.2.1, @mastra/daytona@0.9.0, @mastra/docker@0.6.1, @mastra/e2b@0.10.0, @mastra/modal@0.5.1, @mastra/platform-workspace@1.4.1, @mastra/railway@0.7.0, @mastra/vercel@1.4.1

@mastra/ai-sdk@1.10.0

Minor Changes

  • Added withSseHeartbeat() to the public API so you can keep server-sent event streams alive outside of chatRoute(). (#22116)

    If you build the response yourself in a Next.js, Astro, Nuxt, or SvelteKit route handler, proxies can drop the connection when the stream sends no bytes during a long reasoning burst or slow tool call. chatRoute() already avoided this with its heartbeatMs option, but the underlying helper was not exported.

    Before

    return createUIMessageStreamResponse({ stream });

    After

    import { withSseHeartbeat } from '@mastra/ai-sdk';
    
    return withSseHeartbeat(createUIMessageStreamResponse({ stream }), 15000);

    assertValidHeartbeatMs() is exported alongside it so you can validate a user-supplied interval before streaming. Closes #21954.

@mastra/clickhouse@1.16.0

Minor Changes

  • Added support for filtering scores by metadata key-value pairs in listScores. (#22047)

    const result = await storage.listScores({
      filters: { metadata: { env: 'prod' } },
    });

    Each top-level metadata key is matched with exact equality against the stored value. Nested objects and arrays compare structurally (key order doesn't matter) with no partial/subset matching, and an empty metadata filter is a no-op.

    Also in: @mastra/pg@1.22.0

Patch Changes

  • Fixed ClickHouse observability discovery refreshable materialized views failing with error 36 when their target tables use a Replicated engine inside a non-Replicated (Atomic) database. The discovery views now refresh in APPEND mode, and existing deployments with the old view definitions are migrated automatically on startup (only the views are recreated; discovery data is kept). Fixes #21168 (#22103)

@mastra/client-js@1.42.1

Patch Changes

  • Agent controller streams now recognize thread_title_updated, so it is narrowed by isKnownAgentControllerEvent() and reaches typed consumers like every other known event. (#22156)

@mastra/cloudflare@1.6.3

Patch Changes

  • Fixed Cloudflare KV storage silently dropping data once a table grows past 1000 keys. Listing threads, deleting threads, and clearing tables now read every page of keys from Cloudflare instead of only the first one, so large stores no longer lose threads or leave orphaned messages behind. Also fixed writes through the REST API, which Cloudflare rejected with a 'metadata must be valid json' error. Fixes #22015 (#22204)

@mastra/code-sdk@1.5.0

Minor Changes

  • Added browser-safe thinking command helpers so Mastra Code interfaces can share command parsing, model capabilities, and default resolution. (#22198)

    import { parseThinkCommand, resolveDefaultThinkingLevel } from '@mastra/code-sdk/thinking';
    
    const action = parseThinkCommand('high');
    const fallback = resolveDefaultThinkingLevel({ globalDefault: 'medium', modeDefaults: { plan: 'high' } }, 'plan');
  • Added Parallel as a configured web search provider in Mastra Code, alongside Tavily. Set PARALLEL_API_KEY to enable Parallel-backed web_search and web_extract tools, and pick your default provider in the TUI under /settings → Web search provider (providers are selectable only while their API key is configured; Auto uses the first configured key). (#22216)

    PARALLEL_API_KEY=your-api-key npx mastracode --prompt "Use web_search to find the latest Mastra release"
  • Factory sessions now get a real thread name on their first turn. Mastra's built-in title generation is enabled for them, so a thread is named from the first exchange with the same cheap model the observational-memory observer uses. (#22156)

    Before, a factory session kept whatever name it was created with — the raw first prompt, or nothing at all for work sessions, which fell back to showing their branch — until the observer got far enough into the conversation to name it. Naming now happens on the first turn; the observer still refines it as the thread grows.

    TUI sessions are unchanged: they keep being named by the observer, and pay for no extra call.

@mastra/daytona@0.9.0

Minor Changes

  • Added a secrets option to DaytonaSandbox for injecting Daytona Secrets into sandboxes. Map environment variable names to Daytona Secret names and the real value is substituted into HTTPS request headers at egress — the raw credential never enters the sandbox. (#22322)

    const sandbox = new DaytonaSandbox({
      secrets: {
        GITHUB_TOKEN: 'github-token',
      },
    });

    Closes #22314

  • Added computer-use support to DaytonaSandbox. Workspaces backed by Daytona can now take screenshots, control the mouse and keyboard, inspect the display, and open a noVNC viewer through the standard computer tools. (#21701)

    const sandbox = new DaytonaSandbox();
    await sandbox.start();
    
    await sandbox.computer.leftClick(100, 200);
    const screenshot = await sandbox.computer.screenshot();

    Desktop services start lazily on the first computer operation. Set computerUse: false to disable the capability or computerUse: { autoStart: false } to manage those services directly.

Patch Changes

  • Fixed a leak where every command left its process handle behind, by removing Daytona's own executeCommand in favour of the shared one, which releases them. (#21984)

    Command results now match every other provider: command holds the full command string, and the separate args array is gone.

    const result = await sandbox.executeCommand('echo', ['hello']);
    // before: result.command === 'echo',       result.args === ['hello']
    // after:  result.command === 'echo hello', result.args === undefined
  • Starting a sandbox now reports whether it created a fresh sandbox or reconnected to an existing one, so an onStart handler can run first-time setup only when it's actually needed: (#21984)

    new E2BSandbox({
      id: 'session-1',
      onStart: async ({ outcome }) => {
        if (outcome === 'created') await cloneRepo();
      },
    });

    Also in: @mastra/e2b@0.10.0, @mastra/platform-workspace@1.4.1, @mastra/railway@0.7.0

@mastra/deployer@1.62.0

Patch Changes

  • Layer default dotenv files from base to environment-specific overrides and preserve inherited shell environment variables in mastra dev. (#21927)

    For example, when .env contains API_URL=https://api.example.com and .env.local contains API_URL=http://localhost:3000, Mastra loads both files and uses the .env.local value.

@mastra/duckdb@1.6.3

Patch Changes

  • Fixed metadata filters conflating value types — { count: 5 } no longer matches a stored string '5', and filtering on null metadata values now works. Nested object filters now compare structurally, so key serialization order doesn't affect matching. (#22047)

@mastra/e2b@0.10.0

Minor Changes

  • Added deterministic reattach to E2B sandboxes by provider sandbox ID. Pass the persisted E2B sandbox ID via the new sandboxId option (or clone({ sandboxId })) and start() connects to that exact sandbox — resuming it if paused — instead of discovering by logical id metadata. Only a typed "sandbox gone" error falls back to the usual lookup-or-create path; auth, quota, rate-limit, timeout, and network errors now propagate instead of silently creating a duplicate sandbox. The resolved provider ID is exposed via the new sandbox.sandboxId property so it can be persisted across restarts. (#22316)

    const sandbox = new E2BSandbox({ id: 'my-workspace', sandboxId: persistedId });
    await sandbox.start();
    await save(sandbox.sandboxId); // persist for the next process

    Fixes #22300

  • Added a lifecycle option to E2BSandbox so you can choose what happens when a sandbox times out. Sandboxes still pause by default and resume on next use; pass { onTimeout: 'kill' } to destroy idle sandboxes instead, which suits workspaces whose data is stored outside the sandbox. (#22120)

Patch Changes

  • Fixed E2B sandbox recovery so configured mounts that previously errored are retried after the physical sandbox is replaced. (#22169)

  • Fixed automatic sandbox recovery when E2B reports an ID-specific missing sandbox. (#22215)

  • Improved E2BSandbox extensibility by adding protected SDK creation, connection, and template resolution hooks. Providers built on E2B can now select a specialized SDK sandbox without changing existing E2B behavior. (#21707)

@mastra/e2b-desktop@0.1.0

Minor Changes

  • Added @mastra/e2b-desktop, a computer-use sandbox provider backed by E2B Desktop. It combines E2B command, process, file, and reconnection support with screenshot, mouse, keyboard, screen information, and authenticated noVNC tools. (#21707)

    const sandbox = new E2BDesktopSandbox({ resolution: [1280, 720] });
    const workspace = new Workspace({ sandbox });

    The provider also exports the underlying desktop SDK through sandbox.desktop for desktop-specific operations.

@mastra/editor@0.14.1

Patch Changes

  • Fixed editor reads to preserve published versions after draft updates and honor draft, version ID, and version number selection. (#22179)

@mastra/elasticsearch@1.4.0

Minor Changes

  • Added ElasticSearchStore, an Elasticsearch storage adapter that implements the memory, workflows, and scores storage domains. It shares the same connection config as ElasticSearchVector ({ id, client } or { id, url, auth }), so one Elasticsearch cluster can now power agent memory, workflow snapshots, scores, and semantic recall. See #21757 (#22269)

@mastra/elysia@0.1.0

Minor Changes

  • Added an Elysia server adapter. Use the new @mastra/elysia package to run a Mastra server inside an Elysia app. (#22274)

    import { Elysia } from 'elysia';
    import { MastraServer } from '@mastra/elysia';
    import { mastra } from './mastra';
    
    const app = new Elysia();
    const server = new MastraServer({ app, mastra });
    
    await server.init();
    
    app.listen(4111);

    Also in: @mastra/server@1.62.0

Patch Changes

  • Added the convertCustomRoutesToOpenAPIPaths export to @mastra/server/server-adapter so server adapters can include custom API routes in generated OpenAPI documents. (#22274)

    Also in: @mastra/server@1.62.0

@mastra/factory@0.10.0

Minor Changes

  • Added a Regenerate title action to a session's ⋯ menu in the sidebar. It re-names the conversation with the model that names threads on their own — the owner's observational-memory observer model — and mirrors the new name onto the session row. (#22156)

    Use it on sessions that were started before automatic naming, or whose name no longer matches where the conversation went. Naming runs as the session's owner, so it resolves their stored provider credentials, and it never materializes a workspace: a session that has been closed for weeks can still be re-named.

  • Factory session names in the sidebar now follow the thread's generated title instead of freezing on the raw first prompt. (#22156)

    A chat session used to keep the exact text you first typed ("Tell me what have been done in the factory since…"), and a work session showed its branch ("factory/pr-22160"), even though Mastra had already named the underlying thread "PR review approval". The session row now mirrors that title from whichever namer produced it — the first turn, the observational-memory observer, or an explicit rename — and reconciles against the stored thread title whenever the session is reopened, so sessions started before this also get named.

  • Added encryption at rest for Factory-managed provider credentials, GitHub PATs, and integration OAuth tokens, including automatic migration and key rotation support. (#22152)

  • Card details open in place (#22257)

    Clicking a board card expands it over itself instead of opening a centered dialog, so you keep your place in the column. The panel carries the card's labels, stage, related cards, activity and the source's own description — the GitHub issue or pull request body, the Linear issue description — with the same actions the card menu offers. It is as tall as what it holds, so a card whose source has no description opens onto a short panel and a description arriving from the fetch grows the box into place; re-opening a card paints from cache. Everything the card already showed keeps its exact place while the box grows and folds back around it; only the description and the actions are staged in. A link to the card's source, a collapse button and the actions menu sit in the panel's top corner, and the main action spans the footer — which is “Open session” when the card already has one, instead of offering to start a duplicate.

    Descriptions are read through the Factory server with the org's own GitHub installation and Linear connection, scoped to the sources bound to that Factory project, so no provider token reaches the browser and a board only ever reads its own sources.

    A faster board, and a way to search it

    Boards with hundreds of cards no longer redraw all of them on every poll: each column renders a page of cards at a time and reveals the next as you scroll it, offscreen cards skip layout and paint, relationships between cards resolve in one pass instead of once per card, and the activity feed reads a bounded window of the audit trail rather than replaying the project's whole history on every visit.

    Because a column now shows a page at a time, the board filter bar carries a search: type a card's title or its issue key (#812, ENG-42) and matching cards surface however deep they sat. It narrows before the paging, composes with the teammate and label filters, and lives in the URL (?q=), so a narrowed board is a link you can share.

  • Added a durable Factory action center for unresolved automation failures and proposed work waiting for approval. Per-user read/archive receipts survive reloads, while retries and canonical reconciliation resolve failures for every project member. (#22021)

    Historical decision state is repaired on startup: accepted transitions become succeeded, obsolete terminal work and proposals become superseded, and active unresolved failures remain failed. Retry is offered only when the persisted failure code allows it.

    Before

    // Failed automation and proposed runs were visible only on their board cards.

    After

    const attention = await fetch(`/web/factory/projects/${factoryId}/attention`).then(response => response.json());
    // attention.items: per-user unresolved failures
    // attention.approvalCount: project-wide proposed work
  • Improved the Factory audit log with a density timeline, category filters, responsive rows, and automatic history loading. Intake binding changes now appear in the affected project's audit history. (#22023)

    const response = await fetch(`/web/factory/projects/${factoryProjectId}/audit?actions=factory.run.started&limit=50`);
    const page = await response.json();

Patch Changes

  • Improved the work and review boards on small screens. Column headers now stay pinned while scrolling below the desktop breakpoint, and columns use a fixed width instead of scaling with the viewport. (#22329)

  • Factory pull request review reports now show their selected model and reasoning setting: (#22238)

    Review runtime: openai/gpt-5.6-sol, reasoning setting: high.
    
  • Improved how streamed replies move: one document, one pace, and a transcript that stops shifting under the reader. (#22299)

    Fixed

    • A reply streams in the order it was written, on one clock: prose reveals word by word (thinking passages included), tool rows and cards land between the words they were written between, and a burst of parallel calls cascades in one at a time instead of dropping as a block.
    • Rows no longer replay their entrance mid-run. Adopting the server's message id, the run rotating its message at a step, a slot getting its content, or a tool run ending all used to remount rows the reader was watching — a row now keeps its bubble, its element and its place from the moment it lands.
    • Reply text split across content blocks is parsed as one markdown document, so a list item cut mid-stream no longer renders as an empty bullet followed by a paragraph.
    • Focusing the window mid-run no longer duplicates the streaming reply or jumps the scroll.
    • Steering a running reply no longer clears the view: the steer slides in under the stream instead of parking at the top with an empty screen of room beneath it, and steering while scrolled up brings the reader back to the live end.
    • An agent question fills its reserved slot without rebuilding the text around it, and the "Thinking" line settles its sweep and fades under the first output instead of vanishing mid-sweep.

    Changed

    • Sending a message parks it near the top of the view with most of the screen reserved beneath, so the answer grows into empty space and nothing moves while it fits that room.
    • Opening a thread that is still answering follows the stream from the live end, instead of holding the reading position it restored.
    • A run of tool calls the reader watched arrive stays expanded. Compacting into a "N steps" row is what reloaded history does; a live turn stays as it played, including in a session opened mid-run.
    • The timestamp and copy button land once, under the finished reply, and copy the whole answer — instead of once per persisted step, mid-run.
    • Long transcripts redraw only the entry a token changed, so streaming stays responsive.
  • Fixed Factory lifecycle automation so feature requests and other non-bug work require explicit human approval before entering Planning or Execute, including when automatic runs are enabled. (#22304)

  • Fixed automated runs for manually created board cards. Moving a manual card into Planning or Building no longer fails with 'Factory skill invocation requires a supported issue or pull request identifier'. Manual cards now start on a stable factory/item-<id> branch, even without a provider identity. (#22114)

  • Improved scrolling on the factory work and review boards. The filter bar and column headers stay pinned while you scroll, the board scrolls natively edge to edge instead of inside nested scroll areas, and it no longer opens scrolled partway across the columns. (#22326)

  • Fixed retried Factory skill runs so they deliver a fresh kickoff after execution errors while preserving duplicate protection during lease recovery. (#21926)

  • Fix: Attribute approved Factory runs to the approver, not the repo connector. The approve route now persists approved_by on the deferred decision, session preparation prefers the approver's identity over the repository connector's, and prepareRunStart stamps only the starting role's session instead of repointing every role. Closes #22254. (#22256)

  • Repair Factory-authored pull request review cards when GitHub provenance and the opened webhook arrive out of order, preserving any parent relationship already assigned. (#22167)

  • Fix skill kickoffs delivered into a terminating run being consumed without execution. The decision dispatcher now observes the run's end after a kickoff is delivered into an active run: if that run finishes without executing the kickoff, it is redelivered to wake the idle session, and if the run never ends before the observation deadline the pending start or decision is failed for retry instead of being silently completed. (#22263)

  • Added display names and avatars to Factory user session owner information. (#22341)

  • Intake listings no longer fail as a whole when one provider is down. GET /web/intake/sources and GET /web/intake/items now query every connected provider concurrently and isolate the ones that error, returning what the healthy providers answered plus a failures entry per broken provider so the UI can show a per-source error instead of an empty board. (#22289)

    {
      "sources": [{ "integrationId": "github", "id": "repo-1", "name": "acme/app", "type": "repository" }],
      "failures": [{ "integrationId": "linear", "message": "Linear token expired" }]
    }

    A provider that hangs is given up on after 15 seconds and reported the same way, so an unresponsive one can't hold the request open either.

    A provider that fails mid-pagination keeps the cursor it came in with, so the next page resumes where it left off instead of replaying its first page.

  • Factory skill playbooks in Settings › Agent › Skills now render as formatted markdown instead of a wall of plain text, with a toggle on hover to read the raw SKILL.md source. Long skills scroll inside the card rather than stretching the page. (#22018)

  • add installable PWA metadata and device icons to the Factory UI (#22051)

  • Fixed the Factory board and session sidebar reshuffling while you read them. Cards and sessions are now ordered by when they were created, not by when they were last touched. A background sync or an agent run no longer moves a card. In the sidebar, a session whose pull request is merged or closed now sits below the ones still open, unless its agent is still working or left output you have not read. (#21949)

  • Fixed Factory rule composition so explicitly disabled handlers stay disabled across repeated merges. (#21924)

  • Made secret encryption opt-in instead of mandatory when auth is enabled. MastraFactory.prepare() no longer throws when secretEncryption is omitted with auth on; it logs a boot-time warning and falls back to plaintext credential storage. Providing secretEncryption (for example via FACTORY_CREDENTIAL_ENCRYPTION_KEY) remains the recommended configuration for encrypting stored model-provider keys, custom-provider API keys, and integration secrets at rest. (#22259)

  • Fixed session timing measurements that started too early or missed workspace tool activity. (#22213)

    First interaction time
    Starts on the first user or assistant message. Signal-only messages (skill loads, phase markers, memory reminders) and sessions that fail before a message no longer affect this metric.

    First meaningful tool time
    Starts when the first workspace tool completes successfully. File operations and workspace searches count even when no shell command runs. Approval-denied and abort-while-parked tool completions are excluded because the tool never actually ran.

  • Fixed the Factory board's Intake column claiming "Intake is clear" when a candidate feed had actually failed. A GitHub or Linear feed that errors now shows what went wrong with a Retry, and the Linear reconnect notice keeps its own message. (#22289)

  • Fixed Factory issue triage to update its existing handoff comment across retries. (#22303)

  • Fixed merged pull requests only reaching one of the two Factory cards that track them. A merge now both moves the Review card to Done and asks the work item that opened the pull request to assess whether its work is finished, no matter which card the merge event resolved to. (#22135)

@mastra/fastembed@1.3.0

Minor Changes

  • Added multilingual embedding support to FastEmbed. The multilingual E5 Large model is now available as two role-specific embedding models: use multilingualE5LargePassage for text you index and multilingualE5LargeQuery for search text. Both produce 1024-dimensional vectors, so your vector index must be created with matching dimensions. (#22276)

    import { Memory } from '@mastra/memory';
    import { fastembed } from '@mastra/fastembed';
    
    const memory = new Memory({
      embedder: fastembed.multilingualE5LargePassage,
    });

@mastra/github-signals@0.3.0

Minor Changes

  • Added intent-aware GitHub pull request subscription modes. Review mode follows code revisions, authorized comments, review-thread state, and terminal PR state without CI or mergeability noise, while omitted modes retain working behavior. (#21542)

    await githubSignals.subscribeThreadToPR({ threadId, resourceId, pr, mode: 'review' });

@mastra/hono@1.7.2

Patch Changes

  • Fixed server.middleware and middleware added via mastra.setServerMiddleware() being silently ignored when Mastra is served through a server adapter instead of mastra dev / mastra build. (#22161)

    Hono-based adapters (@mastra/hono, and @mastra/next / @mastra/tanstack-start which build on it) now register the configured middleware during init(), with the same guarantee as the built-in server: user middleware never runs on routes declared public with requiresAuth: false. Adapters for other frameworks (Express, Fastify, Koa) cannot run Hono middleware handlers and now log a warning at startup instead of silently ignoring the configuration.

    Also fixed custom routes with requiresAuth: false not being treated as framework-public when the adapter derives its route auth configuration from the Mastra instance instead of receiving it in the constructor.

    Fixes #21869

    Also in: @mastra/next@0.2.18, @mastra/server@1.62.0, @mastra/tanstack-start@0.2.18

@mastra/inngest@1.8.8

Patch Changes

  • Fix durable agent resume targeting and dispatch error handling on Inngest. (#21742)

    Resume labels (the toolCallId a suspended tool call registers) were dropped when a suspension crossed a nested workflow boundary, so createInngestAgent().resume() had nothing to target with. It also addressed only the outer step, leaving the engine to guess which suspension inside that step to resume — a guess that fails with Multiple suspended steps found when several are parked.

    Nested suspensions now carry their resume labels up to the parent snapshot, resume() accepts a toolCallId naming which suspended tool call to resume, and the resume event now addresses the full path down to the suspended leaf instead of just the outer step. If the supplied toolCallId is unknown, or if it is omitted while more than one suspension is parked, resume() throws immediately and lists the available toolCallIds instead of silently resuming the wrong one.

    resume() also awaits acknowledgement of the resume event dispatch before returning, so a failed send rejects the call instead of only surfacing later as a terminal stream error.

  • Fixed Inngest durable agent runs writing two separate traces. Spans created before the run starts — input processors and memory recall — now nest under the single agent run span instead of being dropped or landing on a second trace, and the agent run span input shows the messages you passed in rather than internal message-list state. (#19841) (#22118)

  • Fix durable agent dropping the per-call actor signal, and centralize durable trigger/resume event construction. (#22129)

    createInngestAgent() accepted an actor option on stream() but never forwarded it into the workflow trigger event, so authorization checks downstream saw no actor. resume() did not accept an actor at all. Both now match InngestRun: actor is supplied per call and is never read back from the persisted snapshot.

    The trigger and resume event payloads were previously built independently in run.ts and in the durable agent wrapper, which is how the two paths drifted apart. Both now build their events through shared helpers so a new per-call signal cannot be added to one path alone.

@mastra/libsql@1.22.0

Minor Changes

  • Added native application collection counts so totals no longer load matching rows. (#22021)

    Before

    const total = (await storage.ops.findMany('jobs', { status: 'failed' })).length;

    After

    const total = await storage.ops.count?.('jobs', { status: 'failed' });

    Also in: @mastra/pg@1.22.0

  • Added embedded-replica sync support to LibSQLStore. You can now pass syncUrl and syncInterval to keep a local database file synced with a remote libSQL primary (for example Turso), matching the options LibSQLVector already supports. (#22261)

    import { LibSQLStore } from '@mastra/libsql';
    
    const storage = new LibSQLStore({
      id: 'libsql-storage',
      url: 'file:./replica.db',
      syncUrl: 'libsql://your-db-name.turso.io',
      authToken: process.env.TURSO_AUTH_TOKEN,
      syncInterval: 60,
    });

    See #21994

  • Added a reusable SQLite client contract so storage adapters can use compatible SQLite drivers. (#22181)

    import type { SqliteClient } from '@mastra/libsql';
    
    const client: SqliteClient = createCompatibleSqliteClient();

Patch Changes

  • Added native Turso Database file storage for Mastra agents, workflows, memory, and other storage domains. (#22181)

    import { TursoStore } from '@mastra/turso';
    
    const storage = new TursoStore({
      id: 'local-storage',
      path: './mastra.db',
    });

    Also in: @mastra/turso@0.1.0

  • Workflow snapshot upserts no longer overwrite a previously stored resourceId with NULL when a run is re-persisted without one (for example during resume). (#22105)

    Also in: @mastra/pg@1.22.0

@mastra/mcp@1.17.2

Patch Changes

  • Preserve structured HTTP status and transport codes in aggregate MCP discovery errors while retaining legacy string errors. (#22218)

    const { errorDetails } = await mcp.listToolsWithErrors();
    if (errorDetails.weather?.httpStatus === 503) {
      // Apply a transient-failure policy without parsing the legacy error string.
    }
  • Fixed dynamic prompt providers leaking prompts across authenticated callers. (#22214)

  • Fixed MCP tool results with structured output dropping the result-level _meta from the server's CallToolResult. The metadata (for example _meta.ui.resourceUri, which MCP Apps hosts use to detect and render an app) is now preserved on the returned structured result and can be read with the new getMcpCallToolMeta helper. The existing hidden MCP content channel is also now readable via the exported getMcpCallToolContent helper. Fixes #21278 (#22102)

@mastra/memory@1.28.0

Minor Changes

  • Added opt-in awaited Observational Memory hooks for synchronous cycles. (#22147)

    Set hookExecution: "await" to await lifecycle hooks, stop the observer or reflector when a start hook fails, and receive one paired end callback after cleanup. Async-buffer cycles remain fire-and-forget.

    const memory = new Memory({
      options: {
        observationalMemory: {
          hookExecution: 'await',
          hooks: {
            onObservationStart: async context => {
              await authorizeObservation(context);
            },
          },
        },
      },
    });

Patch Changes

  • Observational memory now detects observer/reflector output where a multi-line block repeats many times (a model repetition loop). Previously such output could slip past the degenerate-output check and balloon stored observations, causing constant synchronous reflection churn; it is now rejected and retried like other degenerate output. (#22072)

  • Fixed observational memory reflection to keep retrying with stronger compression when a result remains above the token threshold. (#22232)

  • Fixed Observational Memory tracing spans (om.observer, om.observer.multi-thread, om.reflector) never being ended. Unended spans kept their traces retained in exporters that hold a trace open until every span in it finishes — a memory leak with the Datadog bridge, which retains full LLM Observability payloads — and meant Observational Memory tracing never reached any exporter. The spans now end when the observer/reflector run completes, and record the error before ending when it fails. (#22117)

  • Fixed observational memory being silently wiped when every reflection attempt produced empty or degenerate output. Failed reflections now throw and leave existing observations intact. (#22074)

    Threshold-triggered synchronous reflections are no longer re-run against unchanged observations after an attempt that failed or finished still over the reflection threshold. Reflection retries as soon as observations change.

  • Fixed observational memory re-observing stored working memory and overwriting existing values. (#22247)

  • Fix continuationHints being silently dropped when Observational Memory is configured through Memory. Memory._initOMEngine forwards the observation/reflection configs to the OM engine via explicit field lists, and continuationHints (added in #21302) was missing from both — so continuationHints: { suggestedResponse: false } had no effect unless ObservationalMemory was constructed directly. The Observer/Reflector prompts kept requesting the disabled sections and the injection gate kept admitting them. (#22235)

@mastra/observability@1.17.2

Patch Changes

  • Fix PostgreSQL observability writes failing on NUL characters and unpaired Unicode surrogates (#21728)

    Span serialization truncated strings by UTF-16 code unit, so a cut inside an emoji left a lone surrogate that PostgreSQL rejected on the jsonb cast (22P02). NUL characters were rejected as well (22P05). Because observability events are inserted as a single multi-row statement, one malformed field discarded the entire batch.

    Truncation now preserves complete surrogate pairs, and the v-next PostgreSQL observability encoder sanitizes NUL and unpaired surrogates before the jsonb cast, using the same sanitizer that workflow snapshots already rely on. Valid Unicode, including complete emoji, is preserved.

    Also in: @mastra/pg@1.22.0

  • Fixed MastraStorageExporter staying disabled when a configured observability store is temporarily unavailable during startup. The exporter now recovers on a later event, so traces resume without restarting the process. (#21943) (#21957)

  • Added span.endTree() for closing a span together with every descendant span that is still open, so an operation that is abandoned rather than completed can still emit a full trace. The options you pass are applied to every span it closes, so a force-closed child is distinguishable from one that finished on its own. (#22278)

    // Ends the span and any child spans still open beneath it, marking each canceled
    workflowSpan.endTree({ attributes: { status: 'canceled' } });

    Repeat calls to span.end() are now ignored. A span that was force-closed this way keeps the state it was closed with and reports its end exactly once, even if the work it covered finishes later and ends the span again.

@mastra/parallel@0.1.0

Minor Changes

  • Added Parallel Search and Extract tools for Mastra agents through the new @mastra/parallel package. (#22070)

    import { createParallelTools } from '@mastra/parallel';
    
    const tools = createParallelTools();

@mastra/pg@1.22.0

Minor Changes

  • In-progress traces now appear in Studio with PostgresStoreVNext (#22137)

    PostgresStoreVNext previously persisted a span only after it finished, so a long agent run stayed invisible until it completed. It now uses the event-sourced tracing strategy: one row is written when a span starts and another when it ends, and reads collapse those rows back into a single span. Traces show up in Studio while the run is executing, and filtering by running status works.

    This also fixes duplicate traces from durable runs. A run that suspends and resumes opens a second root span on the same trace, which used to render as two separate entries; the trace list now shows the current root only.

    Writes stay append-only, so throughput is unchanged. The span table gains an isPending column, added automatically on init() — no manual migration needed. Closes #22054.

  • Added namespace isolation to PgVector operations so applications can safely reuse vector indexes across tenants. Existing vectors remain available in the default namespace. (#22149)

    await pgVector.upsert({
      indexName: 'documents',
      vectors,
      ids,
      namespace: 'tenant-123',
    });
    
    const results = await pgVector.query({
      indexName: 'documents',
      queryVector,
      namespace: 'tenant-123',
    });

Patch Changes

  • Stop observability filter discovery from re-scanning all history on every refresh (#22136)

    Discovery queries that build Studio's Traces, Logs, and Metrics filter suggestions scanned every span, metric, and log event each time the cache went stale, which grew unbounded with retained data. Refreshes are now bounded to the last 30 days by default (configurable via observability.discovery.lookbackSeconds, 0 restores the previous unbounded behaviour), so the planner can prune partitions instead of reading all of them. Only one process refreshes a given cache entry at a time, so running several server instances no longer multiplies the work, and Studio holds discovery results for five minutes instead of refetching on every page mount.

    Also in: @mastra/playground-ui@51.1.0

  • Fix PgFactoryStorage reads of json columns holding a JSON value that isn't an object. (#22258)

    node-pg parses JSONB through its own type parsers, so the value reaching row deserialization is already a JS value. Deserialization parsed it a second time when it was a string, which threw and failed the whole read. Objects and arrays came back as JS objects and never hit that branch, so the defect stayed hidden until a caller stored a string.

    This surfaced through Factory credential encryption, which stores secrets as an opaque envelope string: saving or reading any credential on Postgres threw Unexpected token ... is not valid JSON, affecting model provider credentials, integrations, and custom providers. libsql was unaffected, since it stores json columns as text where parsing on read is correct.

  • Fixed PgVector scanning every vector table on startup. Constructing a PgVector warms an index cache in the background, and that warmup asked for full index statistics, which include SELECT COUNT(*) per table. On a large index that is a full table scan per index, per process start, and the warmup never used the count it paid for. query(), upsert(), updateVector() and the "has this index changed?" check in createIndex() paid for the same count. (#22180)

    These paths now read only the index metadata they use (dimension, metric, index type, vector type, index configuration), all of which comes from the Postgres catalog at a cost that does not grow with the size of the table.

    describeIndex() is unchanged and still returns an exact count:

    const stats = await pgVector.describeIndex({ indexName: 'embeddings' });
    console.log(stats.count); // exact row count, as before

    Concurrent callers on a cold cache also no longer duplicate the lookup: the first call is shared with everyone waiting on it, and a failed lookup is not cached.

    Fixes #21952.

@mastra/playground-ui@51.1.0

Minor Changes

  • Added a useKeydown hook for keyboard shortcuts (single keys and modifier combos like mod+k) and a useTableKeydown hook for accessible roving-tabindex navigation in tables and lists (arrow keys, PageUp/PageDown, Home/End). (#22284)

  • Improved how streaming transcripts move, and added the arrival primitives behind it. (#22299)

    Fixed

    • Words already read no longer replay their entrance when markdown rebuilds around them, and a reply born streaming animates from its very first word instead of landing as a block that fades in.
    • The reveal clock no longer steps backwards for one frame, which unmounted a settled tool row and cut its shimmer off mid-sweep.
    • The shimmer on running labels is a single band scaled to the element instead of a tiled pattern, and it dissolves into the text colour when the label lands instead of snapping off.
    • MessageScroller follows the last message instead of the end of its box, so the reader is no longer parked on empty space below the conversation. Catch-up while following a stream is softened on the compositor, and opening a turn parks the sent message above the room reserved for its answer — the room decides how high it rests — and lets the answer grow beneath it.

    Added

    • ArrivalScope, useWatched and Arriving: one shared answer to "was the reader watching when this mounted", so every entrance derives from it.
    • useRevealedText: the word-by-word pacing, moved out of MarkdownRenderer so a caller can lay tool rows and cards down in the same rhythm as the prose.

    Migration

    • streaming on MarkdownRenderer no longer paces the reveal — it only means "this text is a prefix still being written". A caller that relied on it for word-by-word pacing pairs the renderer with useRevealedText:
    // before: streaming paced the text word by word
    <MarkdownRenderer streaming={streaming}>{text}</MarkdownRenderer>;
    
    // after: the caller owns the pace; streaming only mends the unterminated tail
    const shown = useRevealedText(text, streaming);
    
    <MarkdownRenderer streaming={streaming || shown !== text}>{shown}</MarkdownRenderer>;
    • The entrance class mastra-markdown-arriving is renamed to mastra-arriving. Anything targeting the old name — a selector, a className — updates to the new one, best by importing it:
    // before
    <div className="mastra-markdown-arriving" />;
    
    // after
    import { ARRIVING_CLASS } from '@mastra/playground-ui/tokens';
    
    <div className={ARRIVING_CLASS} />;

    Changed

    • Shimmer takes active and stays one element across the switch, so nothing inside it remounts when a label lands:
    // before: a different element per state, remounting everything inside on landing
    const Header = status === 'running' ? Shimmer : 'span';
    
    // after
    <Shimmer active={status === 'running'}>{label}</Shimmer>;
  • Added a composable tool-call component for building accessible, collapsible tool activity rows with custom icons, details, status content, and expanded results. (#22183)

    import { ToolCall, ToolCallContent, ToolCallTrigger } from '@mastra/playground-ui/components/ai/tool-call';
    
    <ToolCall status="running">
      <ToolCallTrigger>Running command</ToolCallTrigger>
      <ToolCallContent>Command output</ToolCallContent>
    </ToolCall>;

Patch Changes

  • Added controlled snapshot frame selection to SankeySignals. The component now requires selectedFrameId and onFrameIdChange props, so the host application owns which timeline snapshot is displayed (for example to persist and restore it). Timeline clicks, snapshot playback, and perspective changes all report the new frame through onFrameIdChange. The playground signals overview page resolves the initial frame from the snapshot list and passes it down. (#22187)

  • Improved the Traces page: trace actions (Evaluate Trace, Save as Dataset Item, Add tool mocks) now live in the trace panel header next to the collapse button, removed the empty gap above the traces list when no filters are applied, and replaced the auto-refetch icon button with a labeled "Auto refresh" checkbox (the subtraces toggle is now a "Subtraces" checkbox too). The trace panel now has Details and Scores tabs — Evaluate Trace switches to the Scores tab showing the trace's scores — and the span panel's Scoring tab was removed. The evaluate action is now labeled "Score trace", and the no-traces empty state no longer shows a documentation CTA. The standalone /traces/:traceId page was removed — those URLs now redirect to /traces?traceId=..., and all in-app links point to the query-param form. (#22313)

  • Fixed popups, tooltips, and menus occasionally stretching the page and showing a second pair of scrollbars. Floating elements now use fixed positioning, so a popup that closes or outlives a window resize can no longer grow the document behind it. (#22329)

  • Improved the datasets experience in Studio: creating and editing a dataset now happens on dedicated pages (wrapped in a card) with proper breadcrumbs instead of dialogs, the dataset breadcrumb links to the dataset while a separate arrow opens the dataset switcher, item comparison moved to a path-based URL and is started from a new "Compare with" section in the item side panel, item checkboxes are always visible with contextual actions consolidated into a single "{n} selected" dropdown with a destructive Delete Items entry, experiment rows open the global experiment page (the dataset-scoped experiment route was removed), and the "Run Experiment" button keeps a stable label. Also improved dataset version selection when running experiments (with an inline old-version notice next to the items search and a link-style "Return to latest" action), and dataset item creation with a spacious sidebar and larger JSON editors. (#21910)

  • Added presentTool to the ai/tool-call component set: maps a tool name and its arguments to an icon, a human label, and the salient argument to surface on the row (with special handling for terminal-style tools whose command drives the expanded body). Added ToolCallMono, the monospace body block of an expanded call with a hover copy button, and ToolCallPresentedHeader, the canonical row header (icon, label, detail, failure mark, chevron) so apps no longer assemble it by hand. ToolCallDetail now fades in on its own when it lands inside an ArrivalScope. All moved from Mastra Factory so every studio surface presents tool calls the same way. (#22364)

    import {
      ToolCall,
      ToolCallContent,
      ToolCallMono,
      ToolCallPresentedHeader,
      ToolCallTrigger,
      presentTool,
      stringifyToolValue,
    } from '@mastra/playground-ui/components/ai/tool-call';
    
    const { icon, label, detail } = presentTool(toolName, args);
    
    <ToolCall status={isRunning ? 'running' : 'idle'}>
      <ToolCallTrigger>
        <ToolCallPresentedHeader icon={icon} label={label} detail={detail} />
      </ToolCallTrigger>
      <ToolCallContent>
        <ToolCallMono copyText={stringifyToolValue(result)}>{stringifyToolValue(result)}</ToolCallMono>
      </ToolCallContent>
    </ToolCall>;
  • Fixed code blocks stretching the page. A long line inside a fenced markdown block used to widen everything around it, pushing the layout past the window. A code block now keeps to the width it is given, and scrolls horizontally inside itself when set to overflow="scroll". (#22018)

  • Added keyboard navigation to interactive lists in Studio. Use ArrowUp/ArrowDown to move between rows, PageUp/PageDown to jump by page, and Home/End to reach the first or last row in agents, workflows, tools, datasets, experiments, scores, traces, logs, and other list views. (#22288)

  • Hardened the fix for popups stretching the page: all floating elements now take their fixed positioning from one shared constant, and a test fails if a new component falls back to Base UI's absolute default and could reintroduce the double-scrollbar bug. (#22331)

  • Fixed invisible leftover popups by upgrading Base UI to 1.7.0. Quick repeated hovers could cancel a popup's exit animation and leave it permanently mounted (mui/base-ui#5395); reopened popups could also flash at stale coordinates before repositioning. Both are fixed upstream in 1.7.0. (#22338)

  • Added a score-over-time line chart above the scores table in the trace panel's Evaluations tab (formerly "Scores"), showing one line per scorer with per-scorer averages in the legend. The tab content now stretches to the panel bottom with a scrollable table card, and the trace action was renamed to "Evaluate trace". (#22313)

@mastra/valkey@0.2.0

Minor Changes

  • Add a GLIDE-backed Valkey storage and server cache integration. (#22176)

    import { ValkeyStore } from '@mastra/valkey';
    
    const storage = new ValkeyStore({ id: 'storage', host: 'localhost' });

@mastra/valkey-streams@0.5.0

Minor Changes

  • Add a GLIDE-backed Valkey Streams PubSub and lease provider. (#22176)

    import { ValkeyStreamsPubSub } from '@mastra/valkey-streams';
    
    const pubsub = new ValkeyStreamsPubSub({ url: 'valkey://localhost:6379' });

Other updated packages

The following packages were updated with dependency changes only:

Don't miss a new mastra release

NewReleases is sending notifications on new releases.