github mastra-ai/mastra @mastra/core@1.52.0
July 23, 2026

latest releases: mastracode@0.32.2, mastra@1.20.2, create-mastra@1.20.2...
4 hours ago

Highlights

Sandbox Deployments + One-Step mastra build (New @mastra/deployer-sandbox)

You can now deploy a full Mastra server (including Studio) into networking-capable workspace sandboxes (Vercel/E2B/Daytona) and get a live URL in seconds via the new @mastra/deployer-sandbox. Push-style deployers can opt into deployOnBuild, making mastra build bundle and deploy in one step (SandboxDeployer already opts in).

Workspace Sandbox Networking, Bulk File Uploads, and Cloning (Fleet-Ready Sandboxes)

WorkspaceSandbox gained optional networking.getPortUrl(port) plus writeFiles() for bulk uploads, enabling preview URLs and faster sandbox provisioning; multiple providers now implement this (notably Vercel/E2B/Daytona). Sandboxes also add clone() to create unstarted sibling sandboxes from a template config—ideal for per-project or per-tenant sandbox fleets.

Factory Storage Domains + New Pg/LibSQL “FactoryStorage” Backends

A new FactoryStorage contract and FactoryStorageDomain enable lifecycle-managed, per-domain application storage initialization/readiness/error reporting, rather than a single monolithic store. New backends PgFactoryStorage and LibSQLFactoryStorage persist Mastra agent state plus custom app domains through one shared Postgres pool/LibSQL connection.

New @mastra/factory Package (Software Factory Extraction + Reusable Route Surfaces)

Software Factory storage domains and route surfaces were extracted into a new @mastra/factory package (projects, work items, intake, audit, credentials, integrations, model packs, etc.) so they can be reused outside the Mastra Code web host. It also adds autonomous first-pass skills (triage/plan/review) that auto-run when items enter matching board columns, reducing required human-in-the-loop steps.

Agent Runtime Enhancements: Durable-by-Config, Multi-Turn Evals, and Per-Request Model Overrides

Agents can now opt into durable execution via AgentConfig.durable (no separate createDurableAgent call), and standalone new Agent({ durable: true }) now reliably uses durable paths across stream/generate/resume/approval APIs. runEvals now supports multi-turn conversations (inputs: string[]) plus per-turn assertions (turns) for stronger regression testing, and agent execution/approvals gain request-scoped model overrides (also fixing Studio so each tab can use a different model).

Breaking Changes

  • @mastra/braintrust: Bundled Braintrust SDK upgraded v2 → v3; BraintrustExporterConfig types narrowed (logger/span shims) and root_span_id semantics differ in v3.

Changelog

@mastra/core@1.52.0

Minor Changes

  • mastra build now deploys in one step for push-style deployers. Deployers can opt in with the new deployOnBuild flag on the deployer contract, and the build runs their deploy() right after bundling. SandboxDeployer from @mastra/deployer-sandbox opts in, so configuring it means mastra build bundles your project, deploys it into the sandbox, and prints the live URL. Existing platform deployers are unchanged. (#19577)

    import { Mastra } from '@mastra/core/mastra';
    import { SandboxDeployer } from '@mastra/deployer-sandbox';
    import { VercelSandbox } from '@mastra/vercel';
    
    export const mastra = new Mastra({
      deployer: new SandboxDeployer({
        sandbox: new VercelSandbox({ sandboxName: 'my-preview', ports: [4111] }),
      }),
    });
    mastra build # bundles AND deploys — prints the live URL
  • The 'workers' option on the Mastra class now merges with the default workers instead of replacing them. Passing custom workers (e.g. a polling worker for an integration) no longer silently drops the built-in orchestration and background-task workers — a custom worker only replaces a default when it shares its name, and 'workers: false' still disables all workers. (#19766)

    const mastra = new Mastra({
      // before: only myPoller ran — orchestration was dropped
      // after: myPoller runs alongside the default workers
      workers: [myPoller],
    });
  • Added optional provider-driven authorization for system (non-user) actors, enabling per-agent least privilege after tenant scope has been validated. (#19338)

    System actors (actor: { actorKind: 'system' } or actor: true) skip the user-centric FGA path. Previously the only boundary for them was tenant scope — there was no way to constrain which agent could do what, and no deny path. FGA providers can now opt in to enforce least privilege for those actors.

    What's new

    • IFGAProvider gains an optional requireActor(actor, params) method that throws FGADeniedError to deny.
    • ActorSignal gains optional agentId, permissions, and scope so a provider can identify and constrain the acting agent. permissions reuses the MastraFGAPermissionInput vocabulary — the actor analog of a user's resolved permissions. It is a self-asserted claim: a provider enforcing real least privilege should resolve authoritative grants from a trusted source keyed by agentId rather than trusting it directly.
    • When a provider does not implement requireActor, the existing trusted-actor bypass is preserved exactly — this change is fully backward compatible.

    Before — system actors had tenant scoping but no provider-defined per-agent authorization:

    // Cron/system run: tenant scope is required, but no per-agent limits are possible.
    await agent.generate('run nightly report', {
      actor: { actorKind: 'system', sourceWorkflow: 'nightly' },
    });

    After — a provider enforces least privilege on the acting agent:

    import type { IFGAProvider } from '@mastra/core/auth/ee';
    import { FGADeniedError } from '@mastra/core/auth/ee';
    
    class MyFga implements IFGAProvider {
      // ...existing check / require / filterAccessible...
    
      async requireActor(actor, { resource, permission }) {
        const agentId = actor === true ? undefined : actor.agentId;
        // Resolve authoritative grants from a trusted source keyed by agentId.
        const granted = await this.grantsForAgent(agentId);
        // `permission` may be a single value or an array (needs ANY one of them).
        const required = Array.isArray(permission) ? permission : [permission];
        if (!required.some(p => granted.includes(p))) {
          throw new FGADeniedError(null, resource, permission, 'actor lacks required permission');
        }
      }
    }
    
    // The acting agent is now identified and constrained by its granted permissions.
    await agent.generate('run nightly report', {
      actor: {
        actorKind: 'system',
        agentId: 'nightly-agent',
        permissions: ['agents:execute', 'tools:execute'],
      },
    });
  • Added an optional networking capability and writeFiles() method to the WorkspaceSandbox interface. Sandbox providers that expose public port URLs can now implement networking.getPortUrl(port), which enables preview URLs and sandbox deploys (see the new @mastra/deployer-sandbox package). Use the new supportsNetworking() type guard to detect the capability at runtime. (#19577)

    import { supportsNetworking } from '@mastra/core/workspace';
    
    if (supportsNetworking(sandbox)) {
      const url = await sandbox.networking.getPortUrl(4111);
    }
  • Added onTitleGenerated callback to memory options. When generateTitle is enabled, pass onTitleGenerated in the memory property of agent.generate() or agent.stream() to be notified when the thread title has been generated and persisted to storage. This is a per-request callback, so each request handler can use its own callback with direct access to its response stream — no storage adapter wrapping needed. (#18998)

    Example:

    await agent.stream('Hello', {
      memory: {
        thread: threadId,
        resource: userId,
        onTitleGenerated: title => {
          res.write(`event: title\ndata: ${JSON.stringify({ title })}\n\n`);
        },
      },
    });
  • Update the experimental AgentController message surface to use the canonical MastraDBMessage shape. (#18783)

    The AgentController now emits, persists, and returns DB-native messages where message parts live under content.parts, terminal status lives under content.metadata, and completed tool invocations retain their explicit error state. Signals such as system reminders and notifications now arrive as separate messages with role: 'signal' instead of being flattened into assistant message content.

    This affects the experimental AgentController event and session APIs, including message_start, message_update, message_end, currentMessage, listMessages, listActiveMessages, firstUserMessage, and firstUserMessages.

    agentController.subscribe(event => {
      if (event.type === 'message_end' && event.message.role === 'assistant') {
        // Before: parts were flattened directly onto the message
        // After: read parts and terminal status from the DB-native shape
        const text = event.message.content.parts
          .filter(part => part.type === 'text')
          .map(part => part.text)
          .join('');
        const stopReason = event.message.content.metadata?.stopReason;
      }
    });
    
    // System reminders and notifications are now separate messages
    const messages = await agentController.session.thread.listActiveMessages();
    const signals = messages.filter(message => message.role === 'signal');
  • Added a FactoryStorage contract that owns application storage domains, initializes them with the shared backend, and reports per-domain readiness and initialization errors. (#19681)

    import { FactoryStorageDomain } from '@mastra/core/storage';
    import { LibSQLFactoryStorage } from '@mastra/libsql';
    
    class TasksStorage extends FactoryStorageDomain {
      constructor() {
        super('tasks');
      }
    
      async init() {
        await this.ensureCollections([{ name: 'tasks', columns: { id: { type: 'uuid-pk' } } }]);
      }
    
      async dangerouslyClearAll() {
        await this.ops.deleteMany('tasks', {});
      }
    }
    
    const storage = new LibSQLFactoryStorage({ url: 'file:mastra.db' });
    storage.registerDomain(new TasksStorage());
    await storage.init();
  • Added request-scoped model overrides to agent execution and approvals, and fixed Studio model selection so each tab can use a different model without changing the agent's configured default. (#19749)

    await agent.stream(messages, { model: 'google/gemini-2.5-flash' });
  • Added access to the workspace resolved for an AgentController session. (#19547)

    Use the session-owned workspace when an operation must remain isolated to that session:

    const session = await controller.createSession({ resourceId, scope });
    const workspace = session.getWorkspace();

    Mastra Code workspace resolvers can now accept an isolated read-only skill extension:

    const workspace = await getDynamicWorkspace({
      requestContext,
      skillExtension: {
        id: 'review-skills',
        paths: ['/__review_skills__'],
        createSource: fallback => new ReviewSkillSource(fallback),
      },
    });

    This lets SDK consumers compose additional read-only skill roots into selected workspaces without changing the default workspace skill set.

  • Added an optional clone() method to the WorkspaceSandbox contract, along with the new SandboxCloneOptions type. clone() constructs an unstarted sibling sandbox that inherits the template's configuration (credentials, image, resources) with per-instance overrides — without performing any I/O. This lets one configured sandbox act as a template for a fleet of sandbox clones (for example, one per project). (#19616)

    const template = new RailwaySandbox({ token, environmentId });
    
    // Fresh sandbox clone for a project — provisions on start()
    const projectSandbox = template.clone({
      id: 'mc-project-42',
      env: { GITHUB_TOKEN: token },
      idleTimeoutMinutes: 30,
    });
    await projectSandbox.start();

    LocalSandbox implements clone() out of the box.

  • Added an optional threadId parameter to AgentController session creation so hosts can create or resume a session bound to an exact thread ID. (#19758)

  • Added multi-turn support to runEvals. Data items can now include an inputs: string[] array — each entry is sent sequentially to the agent on the same thread, and scorers see the accumulated output from all turns. (#18395)

    What changed

    • RunEvalsDataItem accepts an optional inputs array for multi-turn conversations
    • Each turn runs agent.generate() with the same threadId, preserving conversation history
    • runEvals injects both a shared threadId and a resourceId (Mastra memory scopes messages by resource + thread, so both are needed for recall); the resource defaults to the generated thread and a caller-provided targetOptions.memory.resource is preserved. thread is now optional in targetOptions.memory since runEvals owns it.
    • Scorers receive the accumulated output messages from all turns
    • Works with gates, thresholds, and all existing scorer configurations
    • Validation rejects empty inputs arrays with a MastraError

    Also added a turns API for per-turn assertions. Instead of a single holistic score over the accumulated conversation (which can hide a broken follow-up turn), each turn colocates its input with gates/scorers that evaluate only that turn's input and output. Per-turn outcomes are reported in result.turnResults and folded into the overall verdict. When the agent has storage configured, per-turn scorer/gate results are persisted like top-level scores. turns is Agent-only and mutually exclusive with input/inputs.

    Example — holistic multi-turn (inputs)

    import { runEvals } from '@mastra/core/evals';
    import { checks } from '@mastra/evals/checks';
    
    const result = await runEvals({
      target: weatherAgent,
      data: [
        {
          inputs: ['What is the weather in Brooklyn?', 'What about tomorrow?', 'Compare the two forecasts.'],
        },
      ],
      scorers: [checks.calledTool('get_weather', { times: 2 }), checks.includes('Brooklyn')],
    });

    Example — per-turn assertions (turns)

    const result = await runEvals({
      target: weatherAgent,
      data: [
        {
          turns: [
            {
              input: 'What is the weather in Brooklyn?',
              gates: [checks.calledTool('get_weather')],
            },
            {
              input: 'What about tomorrow?',
              gates: [checks.calledTool('get_weather')], // must call again this turn
              scorers: [{ scorer: checks.similarity('tomorrow forecast'), threshold: 0.5 }],
            },
          ],
        },
      ],
    });
    
    result.verdict; // folds in per-turn gate/threshold outcomes
    result.turnResults; // [{ index, gateResults, thresholdResults, scores }]

Patch Changes

  • Added a public ensureReady() method to FactoryStorageDomain so consumers can initialize a storage domain without going through a global registry. (#19866)

  • Added a durable field to AgentConfig so an agent can opt into durable execution without a separate createDurableAgent call. Setting durable: true (or durable: { cache, pubsub, maxSteps, cleanupTimeoutMs }) auto-wraps the agent with createDurableAgent when it is attached to a Mastra instance; the factory function remains available for advanced use. (#19371)

    const agent = new Agent({
      id: 'my-agent',
      name: 'My Agent',
      instructions: 'You are a helpful assistant',
      model,
      durable: true, // or: { maxSteps: 10, cleanupTimeoutMs: 60_000 }
    });
    
    export const mastra = new Mastra({ agents: { agent } });
  • Fixed new Agent({ durable: true }) to actually use the durable execution path when used standalone (without being registered on a Mastra instance). (#19632)

    Previously, durable: true only took effect when the agent was attached to a Mastra. Constructing an agent with durable: true and calling agent.stream(...) directly silently ran through the non-durable path.

    Now stream, generate, resumeStream, resumeGenerate, approveToolCall, declineToolCall, streamUntilIdle, resume, recover, listActiveRuns, recoverActiveRuns, observe, and prepare all run through the durable execution path on a standalone new Agent({ durable: true }).

    const agent = new Agent({
      id: 'my-agent',
      name: 'My Agent',
      instructions: 'You are a helpful assistant',
      model,
      durable: true,
    });
    
    // Now runs through the durable execution path.
    await agent.stream('hi');
  • Update provider registry and model documentation with latest models and providers (8a0d145)

  • Fixed model router requests through OpenRouter failing with a 400 "thinking blocks cannot be modified" error when an Anthropic model (e.g. openrouter/anthropic/claude-sonnet-4-6) used extended thinking together with parallel tool calls. The bundled OpenRouter provider was updated from 1.5.4 to 2.10.0, which no longer duplicates reasoning details on each tool call when sending conversation history back to the API. Fixes #19436 (#19493)

  • Fixed bounded process output stalling other requests while high-volume commands continue streaming. (#19600)

  • Fixed goal judge results rendering more than once during live goal runs. (#19613)

  • Ensured that canceled workflow results correctly format and deduplicate their payloads before returning. (#19225)

  • Fixed fine-grained authorization (FGA) checks for DurableAgent execution and tool approvals. (#19338)

    Durable agents now enforce agents:execute

    DurableAgent overrides stream() and generate() to run a workflow instead of the base agent path. Those methods now resolve default and per-call options once, then authorize the same effective actor, request context, and memory resource used for execution.

    Behavior change: with an FGA provider configured, durable runs that were previously allowed through are now checked and can be denied.

    Durable resumes reauthorize each call

    Durable approval and decline methods now forward the trusted request context and selected tool-call ID into resume authorization and workflow resume. The actor is resolved for each call and forwarded to both agent authorization and tool execution. An explicit resume actor replaces the initial actor, while a newly resolved default actor can still apply. If neither exists, the resume uses normal user authorization and fails closed when no user is available.

  • Added optional auth provider capability interfaces and type guards to @mastra/core/server: IOrganizationsProvider (personal organization bootstrap and admin checks), IAuthInit (host-driven initialization with database, public URL, and allowed origins), and IAuthHttpHandler (mounting a provider's own HTTP auth endpoints). Server hosts can use the isOrganizationsProvider, isAuthHttpHandler, and hasAuthInit guards to detect what an auth provider supports and wire routes accordingly. (#19765)

  • Fixed Babel 8 compatibility for build-time transforms. (#19393)

  • Fixed caller-supplied tool providers so selected connectionless tools can initialize and manage user connections. (#19872)

  • Updated the unsupported-adapter error for resource-scoped message listing to include convex in the list of storage adapters that support Observational Memory. (#19474)

  • Added native structured-output support lookup through the model provider registry. (#19440)

    import { modelSupportsStructuredOutput } from '@mastra/core/llm';
    
    const supportsStructuredOutput = modelSupportsStructuredOutput('openai/gpt-5.5');
  • Cap the LLM-provided delegation maxSteps at the sub-agent's own defaultOptions.maxSteps. Previously a supervisor's model could silently raise a sub-agent's step budget past its configured default via the delegation tool's optional maxSteps argument. The supervisor's model can still reduce the budget, and onDelegationStart's modifiedMaxSteps (developer code) still overrides the cap. A warning is logged when a value is capped. (#19529)

  • Fixed requestContext typing inside a tool's execute callback. It is now non-optional, matching runtime behavior: Mastra always provides a RequestContext (creating an empty one when the caller passed none), and when requestContextSchema is defined the context is validated before execute runs — on failure the tool returns a validation error and execute is never called. No more null-checks, throws, or tool factories needed to satisfy the compiler. (#19680)

    const tool = createTool({
      id: 'fetch-doc',
      requestContextSchema: z.object({ documentId: z.string(), userId: z.string() }),
      execute: async (input, context) => {
        // Before: context.requestContext was RequestContext<...> | undefined,
        // forcing ?. / ! / throw even though the runtime guarantees it exists
        // After: typed as RequestContext<{ documentId: string; userId: string }>
        const documentId = context.requestContext.get('documentId'); // string
      },
    });

    Callers of tool.execute(...) are unaffected — passing a context remains optional there. Fixes #19480

  • Fixed DurableAgent throwing ToolNotFoundError when a ToolSearchProcessor meta-tool (search_tools / load_tool) was called. The durable tool-call step now resolves processor-injected per-step tools, matching the regular Agent. (#19578)

  • Improve stored runEvals per-turn scores (follow-up to multi-turn turns). Each persisted per-turn scorer/gate result is now labeled with its turn index (metadata.turnIndex), carries the conversation's shared threadId, and links to that turn's own trace span instead of the item-level span. This lets the scores UI group and label per-turn scores by conversation and turn, and resolve each score to the correct trace. (#19491)

  • Fixed unbounded memory growth during long goal runs. A goal run chains many agent turns inside one stream, and the stream previously kept every chunk, step, tool result, and text delta of the entire run in memory (and in suspend snapshots). Goal evaluations now carry the goal gate's explicit shouldContinue decision, and the stream clears its run-lifetime buffers at each continuing evaluation — the judged turn's messages are already persisted by then. Terminal evaluations (completion, waiting for user, judge failure, budget exhaustion) do not truncate, so the final turn is preserved. (#19663)

    Behavior change for goal runs: run-end results now cover the final iteration of the run (everything after the last continuing evaluation) instead of every turn. Streamed chunks, token usage totals, and persisted messages are unaffected — only the aggregates resolved at the end of the run change. Agents without a goal config are unaffected.

    const agent = new Agent({
      name: 'coder',
      model: 'openai/gpt-5.5',
      goal: { judge: 'openai/gpt-5-mini' },
    });
    
    const stream = await agent.stream('Implement feature X');
    const output = await stream.getFullOutput();
    
    // Before: output.text, output.steps, output.toolCalls, and output.toolResults
    // aggregated every turn of the goal run (e.g. 50 turns concatenated).
    
    // After: they cover the final iteration — the turn(s) judged by the terminal
    // evaluation, i.e. the goal's final answer. Use output.messages (or memory)
    // for the full conversation; output.totalUsage still spans the entire run.
  • Fixed native Agent goals continuing to invoke the judge after an unretried API error. (#19662)

  • Fixed onIterationComplete callbacks receiving empty toolResults after successful tool execution. Tool results now include the matching call ID, tool name, and output from the completed iteration. Fixes #19453. (#19454)

  • Fixed generated files from AI SDK v7 models (e.g. gpt-image-1 image output) being corrupted in stream output and saved message history. The tagged V4 file data is now converted to the flat shape Mastra's stream pipeline and message storage expect. This covers both file and reasoning-file response parts. (#19430)

    Also fixed handling of URL-backed generated files: the URL is no longer mislabeled as base64 in file chunks, UI message streams now emit the URL directly instead of a broken data URI, and reading .uint8Array on a URL-backed generated file now throws a descriptive error instead of returning garbage.

  • Replaced GitHub-specific Mastra Code session state with Factory project and linked-repository identities. This lets SDK consumers represent sessions independently of a source-control provider and select a repository explicitly when sandbox execution is required. (#19849)

    Updated Mastra Code onboarding to be Factory-first: create a Factory by name, then link repositories from your connected source-control installations in a separate step. A Factory is valid with zero linked repositories, and the Board, Metrics, and Audit pages stay available for any server-backed Factory. Factory pages keep project-scoped data separate from repository-scoped intake and provide a repository selector when a Factory has multiple linked repositories. Creating a Factory from a local folder remains available as a secondary option.

    Before

    const state = { githubProjectId: 'project-1', sandboxId, sandboxWorkdir };

    After

    const state = {
      factoryProjectId: 'factory-project-1',
      projectRepositoryId: 'project-repository-1',
      sandboxId,
      sandboxWorkdir,
    };
  • Fixed goal judges changing the parent agent's observational-memory model state. (#19710)

  • Fixed durable agent runs continuing after a tool call was denied by authorization. The run now fails immediately instead of letting the model retry the denied tool. (#19886)

  • Fixed answering an ask_user question failing with a misleading "could not find a suspended run" error when the suspended run died before its state was saved (for example when persisting the suspended snapshot failed). The unresumable question is now retracted, a late answer is ignored, and the original error is surfaced instead. (#19444)

  • Fixed prebuilt agent signals so callers can supply per-run request context. (#19702)

  • Fixed Code Mode executions failing when programs return large results or error messages. (#19878)

  • Aligned A2AAgent streams with the regular Agent stream contract. A2A streams now emit a leading start chunk on fresh runs (skipped when resuming, matching Agent behavior) and the finish chunk now carries the Agent-shaped payload (stepResult.reason, output.usage) so downstream consumers can treat sub-agent streams uniformly. The previous flat finishReason and usage fields are still included for backward compatibility but are deprecated. (#19517)

  • Updated the bundled provider SDKs used by the model router for Cerebras, DeepInfra, DeepSeek, Perplexity, Together AI, and OpenAI-compatible endpoints (including custom provider URLs and the Netlify gateway) to their AI SDK v6-compatible versions. This aligns them with the other providers in the model router (OpenAI, Anthropic, Google, Groq, Mistral, xAI, OpenRouter) which already use v6-compatible SDKs, and picks up upstream provider fixes. No changes to the public API: model strings like cerebras/llama-3.3-70b or deepseek/deepseek-chat keep working as before. Unused v5-track provider SDK bundles (Groq, Mistral, xAI, and the migrated providers above) were removed from the bundle. (#19495)

  • Updated embedding model routing to use the AI SDK v6 provider SDKs (OpenAI, Google, and OpenAI-compatible) while preserving the existing embedding interface. Existing embedding model configurations continue to work without code changes. (#19500)

  • Fixed workflow cancellation for tool-wrapped steps. Cooperative tools can now stop early when run.cancel() is called. Fixes #19599. (#19602)

  • Added a working directory override when deriving local sandboxes so callers can isolate each derived checkout. (#19907)

  • Add jsonPromptInjection: 'auto' to select native structured output when model capability data confirms support and inline JSON prompt injection otherwise. Scorer judges use this automatic path by default while preserving explicit jsonPromptInjection overrides and fallback retries for unexpected provider failures. (#19442)

  • Added checkpointName to sandbox derive options so providers can attach durable checkpoint identities to derived sandboxes. (#19907)

  • Fixed dataset item writes to reject circular payloads with a clear validation error before storage, instead of failing with database-specific serialization errors. Silently lossy JSON values (nested undefined, functions, symbols, bigints, and non-finite numbers) and non-plain objects (Date, Map, Set, class instances, and custom toJSON() objects) are now also rejected with the offending path, so identical externalId retries can no longer conflict with the persisted payload. Added a public safeStringify utility for serializing values that may contain circular references. (#19603)

    import { safeStringify, ensureSerializable } from '@mastra/core/utils/safe-stringify';
    
    const value: Record<string, unknown> = { prompt: 'hello' };
    value.self = value;
    
    safeStringify(value); // '{"prompt":"hello","self":"[Circular]"}'
    ensureSerializable(value); // { prompt: 'hello', self: '[Circular]' }
  • Remove leftover branches that selected the evented workflow engine inside the regular Agent loop. The agentic-execution workflow now always uses the in-process workflow engine, and Agent no longer maintains an ephemeral Mastra (with its own pubsub and startWorkers() lifecycle) just to host the evented path. This removes a class of subtle behavior differences between the two engines from Agent.stream() / Agent.generate() and simplifies the suspend/resume scope lifecycle. (#18666)

    No public API changes. The evented workflow infrastructure itself is unchanged and continues to be used by background tasks, notifications, the score-traces workflow, and other subsystems that explicitly opt into it.

  • Fixed requestContext typing in the dynamic skills agent option. When an agent defines requestContextSchema, the skills callback now receives a typed RequestContext (with key autocomplete and typo checking), matching instructions, tools, memory, and the other dynamic options. Previously it was always RequestContext<unknown>. (#19554)

    const agent = new Agent({
      requestContextSchema: z.object({ documentId: z.string(), userId: z.string() }),
      // Before: requestContext was RequestContext<unknown> — any key compiled
      // After: requestContext is RequestContext<{ documentId: string; userId: string }>
      skills: ({ requestContext }) => {
        requestContext.get('documentId'); // typed as string
        return ['./skills/basic'];
      },
    });

    Fixes #19553

  • Fixed thread cloning to preserve configured memory state, including observational memory progress. (#19917)

  • Fix the createDurableAgent JSDoc examples to construct RedisServerCache with a connected Redis client ({ client }) instead of a { url } config the class does not accept. (#19569)

  • Added stable metrics and logs capability reporting for observability storage. The system packages response now includes observabilityStorageCapabilities with metrics and logs flags, enabling capability-based detection that is resilient to bundler-generated constructor name changes. (#19305)

    const packages = await client.getSystemPackages();
    console.log(packages.observabilityStorageCapabilities?.metrics); // true
    console.log(packages.observabilityStorageCapabilities?.logs); // true

    Studio now uses the capability response instead of relying on constructor names, with a fallback for older servers.

  • Fixed background tasks never executing when Mastra is used as a library without a Mastra server. Previously, background-eligible tool calls (including background subagent delegations) were dispatched but stayed in the running state forever because the task workers only started with a Mastra server (mastra dev or a deployed server). Now the workers start automatically on the first background task dispatch or resume, so background tasks complete in apps that embed Mastra directly (for example Express or Next.js servers). Fixes #19339. (#19476)

  • Fixed durable goal objectives so active execution time is preserved across runs and excludes tool approval waits. (#19837)

  • Fixed a crash on Cloudflare Workers where calling generate() on an Agent not registered to a Mastra instance threw TypeError: this.#intervalHandle.unref is not a function. The scheduler now only calls unref() on its polling interval when the runtime provides it (Node.js); on runtimes where setInterval returns a number (workerd) the call is skipped. Fixes #19462. (#19471)

  • Fixed composite storage initialization so transient failures can retry without restarting the process. (#19697)

  • Moved the server config routes and provider credential helpers into @mastra/factory as a reusable ConfigRoutes class. Route handlers now receive their auth checks through an injected RouteAuth seam and storage domains through constructor options, so hosts other than the Mastra Code web app can mount the same routes. (#19866)

  • Fixed workspace LSP support for TypeScript 7 projects. Code inspection (diagnostics, hover) now works in workspaces using TypeScript 7, including hoisted monorepo and pnpm installations. No configuration changes are needed — existing lsp: true setups keep working, and TypeScript 6 and earlier behavior is unchanged. (#19618)

    Fixes #19601

  • Fixed agents with channels failing on Cloudflare Workers with No such module "chat". (#19570)

    Agents with Slack, Discord, or Telegram channels now deploy to Cloudflare Workers and other serverless targets without extra configuration — channels initialize correctly and webhooks work out of the box. The bundler.dynamicPackages: ['chat'] workaround is no longer needed for Node deploys. Projects that don't use channels still load the Chat SDK lazily.

    Fixes #19254

@mastra/acp@0.4.0

Minor Changes

  • Added a createClient option to createACPTool() and AcpAgent for customizing the ACP client, and fixed a crash when the ACP agent command fails to start. (#19559)

    Custom ACP client (createClient)

    Some ACP agents call custom extension methods (extMethod / extNotification) on the client. The built-in client rejected these with a "Method not found" error and could not be replaced, so those agents were unusable without reimplementing the whole connection. The new createClient option receives the default client and returns the client used for the connection:

    import { createACPTool } from '@mastra/acp';
    
    const tool = createACPTool({
      id: 'grok',
      description: 'Dispatch tasks to Grok',
      command: 'grok',
      createClient: defaultClient =>
        Object.assign(defaultClient, {
          extMethod: async (method, params) => ({}),
          extNotification: async (method, params) => {},
        }),
    });

    The Client type is now re-exported from @mastra/acp for fully custom implementations.

    Spawn failure handling

    If the configured command could not be started (for example the executable does not exist), the child process 'error' event was unhandled and crashed the host process. Tool execution now rejects with the spawn error instead.

    Fixes #19109

Patch Changes

@mastra/ai-sdk@1.6.3

Patch Changes

  • Fixed the AI SDK v6 native approval flow in handleChatStream so approval responses are collected across all assistant messages instead of only the final message. Approving a tool call now resumes its exact run and tool call, multiple approval responses in one request resume sequentially in a single framed response stream, and already-resolved responses from history are safely skipped instead of executing again. (#19528)

  • Fix durable agent streams that could terminate right after the initial start event, leaving the client with an empty response. Streams now continue through step boundaries and deliver the full reasoning, tool, and text output. (#19671)

@mastra/apple-container@0.3.0

Minor Changes

  • Added clone() support to the sandbox providers. clone() constructs an unstarted sibling sandbox that inherits the template's configuration (credentials, image, resources) with per-instance overrides for id and env, so one configured sandbox can act as a template for a fleet of sandbox clones (for example, one per project). (#19616)

    const template = new E2BSandbox({ apiKey, template: 'base' });
    
    const projectSandbox = template.clone({
      id: 'mc-project-42',
      env: { GITHUB_TOKEN: token },
      idleTimeoutMinutes: 30,
    });
    await projectSandbox.start();

    idleTimeoutMinutes is a best-effort hint that maps to each provider's native lifetime knob (Railway idleTimeoutMinutes, E2B/Modal/Vercel timeout in milliseconds, Daytona autoStopInterval, Blaxel TTL duration). Docker and Apple Container ignore it since they have no provider-side idle teardown.

Patch Changes

@mastra/auth-better-auth@1.1.2

Patch Changes

  • Added a deferred instance mode and organization management to MastraAuthBetterAuth so it can be passed directly to a server host without a wrapper adapter. The provider can now be constructed with just a secret and will build its Better Auth instance (including running migrations) against the host database during init. It also bootstraps a personal organization for new users (ensureOrganization), checks organization admin roles (isOrganizationAdmin), and exposes the Better Auth HTTP handler (handleAuthRequest) so hosts can mount it under /auth/api/*. (#19765)

    Before

    import { betterAuth } from 'better-auth';
    import { MastraAuthBetterAuth } from '@mastra/auth-better-auth';
    
    const auth = new MastraAuthBetterAuth({ auth: betterAuth({/* ... */}) });

    After (bring-your-own instance still works)

    import { MastraAuthBetterAuth } from '@mastra/auth-better-auth';
    
    const auth = new MastraAuthBetterAuth({ secret: process.env.BETTER_AUTH_SECRET! });
    // host calls auth.init({ database, publicUrl, allowedOrigins }) during startup
  • Updated the minimum better-auth version to 1.6.13 to pull in the fix for a stored cross-site scripting vulnerability in better-auth's OIDC provider and MCP plugins (GHSA-86j7-9j95-vpqj). (#19584)

  • Fixed Better Auth bearer token authentication and implemented user lookup in @mastra/auth-better-auth (fixes #19110). (#19629)

    Bearer tokens now work after credentials sign-in

    signIn/signUp return Better Auth's raw session token, but Better Auth only accepts signed session cookies. Sending that token as Authorization: Bearer <token> previously always failed authentication. The provider now signs unsigned tokens with the Better Auth secret before verifying the session — matching the semantics of Better Auth's bearer plugin. The session cookie name is also resolved from the Better Auth instance, so secure-cookie setups (__Secure- prefix) work too.

    getUser() and getUsers() are now implemented

    Previously getUser() was a stub that always returned null, breaking Studio user lookup and author enrichment. It now resolves users by ID through Better Auth's internal database adapter, and getUsers() supports batch lookups.

@mastra/auth-studio@1.3.2

Patch Changes

  • MastraAuthStudio now automatically creates a personal organization for users who don't belong to one yet, and can check whether a user is an organization admin — matching the behavior already available in MastraAuthWorkos and MastraAuthBetterAuth. This lets hosts like a self-hosted MastraCode deployment authorize organization-level actions without users needing to manually set up an organization first. (#19858)

  • MastraAuthStudio.ensureOrganization now dedupes concurrent bootstraps for the same user, so parallel tabs or requests for a brand-new sign-in no longer end up creating duplicate personal organizations. (#19858)

  • Changed the default shared API URL for Studio auth from http://localhost:3010/v1 to https://platform.mastra.ai/v1, so deployed instances work against the production platform without extra configuration. Set MASTRA_SHARED_API_URL or the sharedApiUrl option to point at a different environment. Production cookie settings (Secure + Domain=.mastra.ai) are now only auto-enabled when the shared API URL is explicitly configured on .mastra.ai, keeping local development cookies host-only. (#19958)

@mastra/auth-workos@1.6.4

Patch Changes

  • Added organization management and host integration to MastraAuthWorkos so it can be passed directly to a server host without a wrapper adapter. The provider now bootstraps a personal organization for new users (ensureOrganization), checks organization admin roles (isOrganizationAdmin), and resolves its redirect URI from the host public URL during init when not configured explicitly. (#19765)

@mastra/blaxel@0.6.0

Minor Changes

  • Added clone() support to the sandbox providers. clone() constructs an unstarted sibling sandbox that inherits the template's configuration (credentials, image, resources) with per-instance overrides for id and env, so one configured sandbox can act as a template for a fleet of sandbox clones (for example, one per project). (#19616)

    const template = new E2BSandbox({ apiKey, template: 'base' });
    
    const projectSandbox = template.clone({
      id: 'mc-project-42',
      env: { GITHUB_TOKEN: token },
      idleTimeoutMinutes: 30,
    });
    await projectSandbox.start();

    idleTimeoutMinutes is a best-effort hint that maps to each provider's native lifetime knob (Railway idleTimeoutMinutes, E2B/Modal/Vercel timeout in milliseconds, Daytona autoStopInterval, Blaxel TTL duration). Docker and Apple Container ignore it since they have no provider-side idle teardown.

Patch Changes

@mastra/braintrust@1.3.0

Minor Changes

  • Breaking change (#19507)

    Updated the bundled Braintrust SDK from v2 to v3 and replaced the SDK-specific logger and span types with stable Mastra interfaces/shims. Compatible Braintrust v2 and v3 logger and span objects remain supported.

    Braintrust v3 uses a separate W3C trace ID for root_span_id. Mastra's returned spanId remains the Braintrust row ID and span ID for feedback and lookup.

    Migration

    The BraintrustExporterConfig interface changed, so if you were using the braintrustLogger field or the getCurrentSpan() method, those now hold narrower types that are no longer tied to the Braintrust SDK. It is unlikely that this is affecting you but technically a breaking change, which is why it is outlined.

    Applications that independently upgrade Braintrust and use Nunjucks prompt templates should follow the Braintrust v2 to v3 migration guide: https://www.braintrust.dev/docs/sdks/typescript/migrations/v2-to-v3

Patch Changes

@mastra/client-js@1.33.0

Minor Changes

  • Added Agent Learning response types for entity discovery, snapshots, flows, theme details, examples, history, and noise. (#19871)

    import type { ThemeDetailResponse, ThemeFlowResponse } from '@mastra/client-js';
  • Added a requestContext option to agent controller session methods in @mastra/client-js. You can now pass custom request context to sendMessage, steer, followUp, approveTool, and respondToToolSuspension, matching what agent.generate() already supports. The context is merged into the run's request context on the server, so it reaches dynamic instructions and tools. (#19531)

    const session = client.getAgentController('code').session('user-1');
    await session.sendMessage('hello', { requestContext: { userId: 'user-1', tier: 'pro' } });
  • Added exact thread binding when creating AgentController sessions through the JavaScript client. (#19902)

  • Added request-scoped model overrides to agent execution and approvals, and fixed Studio model selection so each tab can use a different model without changing the agent's configured default. (#19749)

    await agent.stream(messages, { model: 'google/gemini-2.5-flash' });
  • Added reconnect support to AgentControllerSession.subscribe() so SSE subscriptions recover after proxy timeouts or transport errors. Fixes #19202. (#19560)

    await session.subscribe({
      onEvent: event => {
        /* handle event */
      },
      reconnect: true,
    });

Patch Changes

  • Fixed AgentController live events and thread history returning serialized message timestamps instead of Date values. (#18783)

  • Fixed AgentControllerSession.subscribe() so it resolves only after the stream is established and rejects when it cannot connect (leaving no background retry loop). Reconnect now applies only after an established stream drops, backs off exponentially, and fires a new onReconnect callback on each re-established stream so consumers can re-sync missed events: (#19580)

    const subscription = await session.subscribe({
      onEvent: event => applyEvent(event),
      onError: error => showDisconnected(error),
      reconnect: { maxRetries: 5, delayMs: 1000, maxDelayMs: 30_000 },
      onReconnect: async () => {
        // Events emitted while disconnected are not replayed — re-sync state.
        applyState(await session.state());
      },
    });
  • Updated generated route types so memory.resource is optional in agent generate and stream request bodies. The server can derive the resource ID from the authenticated user via mapUserToResourceId, so clients no longer need to provide it. Fixes #19518. (#19524)

  • Replace any with unknown in generated route types so clients get real type errors instead of silently unchecked values. Route-types generation now deduplicates shared schemas into reusable type aliases, shrinking the generated route-types.generated.ts from ~94K to ~22K lines (77% smaller). The memory config response now types workingMemory (enabled, scope, template, schema, version) instead of unknown. (#19573)

    If your code read fields from responses that were previously typed any, TypeScript now requires you to narrow them before use:

    // Before: `metadata` was `any`, so this compiled even when unsafe
    const value = response.metadata.someField;
    
    // After: `metadata` is `unknown` — narrow it first
    const metadata = response.metadata;
    if (metadata && typeof metadata === 'object' && 'someField' in metadata) {
      const value = metadata.someField;
    }
    // ...or cast if you know the shape: (metadata as MyMetadata).someField

    Code that reads workingMemory from the memory config response no longer needs casts — config.workingMemory?.enabled, scope, template, schema, and version are now typed directly.

  • Added stable metrics and logs capability reporting for observability storage. The system packages response now includes observabilityStorageCapabilities with metrics and logs flags, enabling capability-based detection that is resilient to bundler-generated constructor name changes. (#19305)

    const packages = await client.getSystemPackages();
    console.log(packages.observabilityStorageCapabilities?.metrics); // true
    console.log(packages.observabilityStorageCapabilities?.logs); // true

    Studio now uses the capability response instead of relying on constructor names, with a fallback for older servers.

@mastra/code-sdk@1.0.0

Major Changes

  • Replaced GitHub-specific Mastra Code session state with Factory project and linked-repository identities. This lets SDK consumers represent sessions independently of a source-control provider and select a repository explicitly when sandbox execution is required. (#19849)

    Updated Mastra Code onboarding to be Factory-first: create a Factory by name, then link repositories from your connected source-control installations in a separate step. A Factory is valid with zero linked repositories, and the Board, Metrics, and Audit pages stay available for any server-backed Factory. Factory pages keep project-scoped data separate from repository-scoped intake and provide a repository selector when a Factory has multiple linked repositories. Creating a Factory from a local folder remains available as a secondary option.

    Before

    const state = { githubProjectId: 'project-1', sandboxId, sandboxWorkdir };

    After

    const state = {
      factoryProjectId: 'factory-project-1',
      projectRepositoryId: 'project-repository-1',
      sandboxId,
      sandboxWorkdir,
    };

Minor Changes

  • Moved model packs in Mastra Code web to database-backed storage and refreshed the built-in packs. (#19849)

    Model packs are now stored in the Factory database

    When running with a Factory backend, custom model packs are saved in a new model-packs storage domain scoped to your organization instead of the local settings.json file. Local (non-tenant) mode keeps the file-backed behavior.

    Pick from available models

    The settings Model tab now loads the list of available models from a new /web/config/models endpoint, so the Factory default model picker and model pack editor only offer models you actually have credentials for. Model pickers are searchable comboboxes instead of plain dropdowns, and pack activation now resolves the correct scoped session so packs can be activated from settings.

    Default packs updated to the latest model releases

    • Anthropic: build and plan anthropic/claude-fable-5, fast anthropic/claude-haiku-4-5
    • OpenAI: build and plan openai/gpt-5.6
    • Observational memory default model is now google/gemini-3.5-flash
  • Added a Factory default model for server-backed Factories in Mastra Code web. Set it in Settings under the Model tab and every factory run (like issue triage) starts on that model. The Model tab now also hosts model packs, replacing the separate Packs tab — packs stay session-scoped while the default model is stored on the Factory project itself. (#19849)

  • Added an input processor extension for embedding surfaces while preserving Mastra Code's required processors. (#19702)

  • Added support for injecting pre-built storage and vector store instances into Mastra Code. MastraCodeConfig.storage now accepts a MastraCompositeStore instance in addition to a storage config, and the new MastraCodeConfig.vector slot accepts a MastraVector instance. When an instance is provided it is used as-is — no connection test or LibSQL fallback — so hosted deployments can share a single Postgres connection pool between Mastra storage and application tables. (#19623)

    Before

    await createMastraCode({ storage: { backend: 'pg', connectionString } });

    After

    const storage = new PostgresStore({ id: 'code-storage', connectionString });
    const vector = new PgVector({ id: 'code-vectors', connectionString });
    await createMastraCode({ storage, vector });
  • Add goal execution to the headless runMC API. Goal runs use the same GoalManager and system-reminder signal path as the TUI and resolve on terminal goal_evaluation events without manual continuation messages. (#19441)

    const run = runMC({
      controller,
      session,
      goal: {
        objective: 'Implement and verify the requested change',
        judgeModelId: 'openai/gpt-5-mini',
        maxRuns: 20,
      },
    });
    
    for await (const event of run) {
      console.log(event.type);
    }
    
    const result = await run.result;
  • Add browser-based OAuth authentication for HTTP MCP servers to Mastra Code. (#19467)

    When an HTTP MCP server rejects a connection with an authorization error, the
    /mcp selector now shows a "needs auth" badge and an Authenticate action.
    Choosing it opens the provider's consent page in the browser and completes the
    OAuth 2.1 authorization-code flow (PKCE + Dynamic Client Registration) over a
    loopback callback server, persists the tokens, and reconnects — no manual
    configuration required for a bare { "url": ... } server entry. A Cancel
    authentication
    action aborts an in-flight flow and returns the server to the
    needs-auth state.

    The server manager gains authenticateServer(name) and
    cancelServerAuthentication(name), McpServerStatus gains an optional
    needsAuth flag, and the OAuth redirectUrl in MCP server config is now
    optional (it defaults to a stable loopback URL). The config also accepts
    callbackPort as a shorthand that synthesizes
    http://localhost:<callbackPort>/callback, the Claude Code / Codex
    convention, so configs written for those clients (like Slack's official MCP
    plugin config) work verbatim. callbackPort and redirectUrl are mutually
    exclusive.

    const server = manager.getServerStatuses().find(s => s.name === 'supabase');
    if (server?.needsAuth) {
      // Opens the consent page in the browser, completes the OAuth flow, and
      // resolves with the reconnected server status.
      const status = await manager.authenticateServer('supabase', {
        onAuthorizationUrl: url => openInBrowser(url),
      });
      console.log(status.connected);
    
      // Abort an abandoned browser flow and return the server to needs-auth:
      // await manager.cancelServerAuthentication('supabase')
    }
  • Added step-based OAuth APIs for browser-driven provider sign-in and tenant-aware credential resolution. Hosted applications can now inject a credential store so each request resolves the caller's credentials without copying stored secrets into process environment variables. (#19638)

    import { startAnthropicLogin } from '@mastra/code-sdk/auth/providers/anthropic';
    
    const { url, verifier } = await startAnthropicLogin();
  • Added access to the workspace resolved for an AgentController session. (#19547)

    Use the session-owned workspace when an operation must remain isolated to that session:

    const session = await controller.createSession({ resourceId, scope });
    const workspace = session.getWorkspace();

    Mastra Code workspace resolvers can now accept an isolated read-only skill extension:

    const workspace = await getDynamicWorkspace({
      requestContext,
      skillExtension: {
        id: 'review-skills',
        paths: ['/__review_skills__'],
        createSource: fallback => new ReviewSkillSource(fallback),
      },
    });

    This lets SDK consumers compose additional read-only skill roots into selected workspaces without changing the default workspace skill set.

Patch Changes

  • Added on-disk verification to the update utilities: runUpdate now returns the package manager's stderr, and the new performUpdate locates the running install, delegates the update to the tool that owns it (for example vite-plus), verifies the on-disk version when available, and reports when a readable installed version remains unchanged. (#18792)

  • Fixed Moonshot AI API key resolution so keys saved via /api-keys (MOONSHOT_API_KEY) work when selecting moonshot models (#19655)

  • Fixed provider request history repair so incompatible tool-call IDs are sanitized and retried instead of being blindly resent after a provider rejects the request (#19969)

  • Fixed goal duration so it persists across pauses and process restarts. (#19837)

  • Fixed session thread cloning failing with "Source thread not found" when the cached dynamic memory instance was bound to a previous storage instance. The memory cache is now scoped to the storage it was created with. (#19969)

  • Fixed Mastra Code retries for EPIPE and closed provider connections. (#19691) (#19692)

  • Fixed ACP clients dropping standalone signal messages such as system reminders and notification summaries, while preserving assistant text deltas across interleaved signals without inserting separators. (#18783)

  • Added a session notification when a GitHub plugin is automatically updated to its latest version (#19943)

    const unsubscribe = pluginManager.onGithubPluginsUpdated(pluginNames => {
      console.log(`Updated plugins: ${pluginNames.join(', ')}`);
    });
    
    // Call during shutdown.
    unsubscribe();
  • Moved custom model providers and custom model packs off settings.json in the factory web app: both now live in the app database (org-scoped rows in deployed mode, a sentinel local scope in no-auth mode). Custom providers saved in the web settings page are picked up by model resolution and the model catalog through a new pluggable custom-providers source in the SDK, so the gateway no longer reads the host machine's settings.json for them, and models from your custom providers appear in the web model pickers. (#19964)

    Hosts that store custom providers elsewhere (like the factory's database) register a source at boot; when none is registered, the SDK keeps reading settings.json as before:

    import { setCustomProvidersSource } from '@mastra/code-sdk/agents/custom-provider-source';
    
    setCustomProvidersSource(tenant => (tenant ? snapshotForOrg(tenant.orgId) : []));
  • Fixed cloned session threads reading from a previous storage instance. The dynamic memory cache now invalidates when the storage or vector instance changes, so thread cloning always uses the current database. (#19966)

  • Added a memory-settings storage domain: observational memory settings (observer and reflector models, thresholds, attachment observation) changed in the web app are now stored in the app database — one row per user — instead of settings.json, and the settings page reads them back from the database. Factory-mounted agent controllers no longer seed observational memory settings from the host machine's settings.json (new disableSettingsOmSeed SDK option), so server sessions start from built-in defaults plus whatever is stored in the database. The OM settings model pickers in the web UI are now searchable comboboxes. (#19964)

    Server embedders that persist memory settings in their own database can opt out of the settings.json seed:

    import { createMastraCode } from '@mastra/code-sdk';
    
    const mastraCode = await createMastraCode({
      cwd: process.cwd(),
      // Don't seed observer/reflector models or thresholds from the host
      // machine's settings.json — sessions start from built-in defaults.
      disableSettingsOmSeed: true,
    });
  • Fixed Amazon Bedrock prompt caching for long Mastra Code conversations. (#19690)

  • Fixed a crash (TypeError: Cannot read properties of undefined (reading 'includes')) when a Mastra store instance is injected into the SDK from a project whose dependency graph contains duplicate copies of @mastra/core. Injected stores are now detected structurally instead of with instanceof, so stores built against a different core copy are recognized correctly instead of being mistaken for a storage config. (#20030)

@mastra/convex@1.4.0

Minor Changes

  • Added Observational Memory support to the Convex storage adapter. Agents using ConvexStore can now enable observationalMemory in @mastra/memory, including async observation buffering and reflections. (#19474)

    To enable it, add the new mastraObservationalMemoryTable to your Convex schema and redeploy:

    import { defineSchema } from 'convex/server';
    import {
      mastraThreadsTable,
      mastraMessagesTable,
      mastraResourcesTable,
      mastraObservationalMemoryTable,
      // ...other Mastra tables
    } from '@mastra/convex/schema';
    
    export default defineSchema({
      mastra_threads: mastraThreadsTable,
      mastra_messages: mastraMessagesTable,
      mastra_resources: mastraResourcesTable,
      mastra_observational_memory: mastraObservationalMemoryTable,
      // ...other Mastra tables
    });

    Then run npx convex deploy (or npx convex dev) so the new table and storage operations are live. All observational memory writes run inside the deployed Convex storage mutation, so buffered-observation swaps and reflection generations are atomic.

Patch Changes

  • Improved type safety of the Convex storage layer. Table names accepted by the internal ConvexDB are now a closed union (core storage tables plus the observational memory table) instead of any string, so table-name typos are caught at compile time instead of silently routing records to the mastra_documents fallback table. The internal observational memory operations no longer take a table-name argument. (#19485)

  • Removed the Convex adapters' only Node builtin dependency. ConvexStore, the vector adapters, and the storage domains now use the Web Crypto API (crypto.randomUUID) instead of importing node:crypto, so @mastra/convex itself no longer requires the Node.js runtime ("use node") to bundle inside a Convex project. (#19498)

@mastra/daytona@0.6.0

Minor Changes

  • Added networking and bulk file upload support to DaytonaSandbox: (#19577)

    • Public port URLs: sandbox.networking.getPortUrl(port) returns the preview URL for a port, enabling preview URLs and deploys with the new @mastra/deployer-sandbox package. Pass public: true for tokenless URLs.
    • Bulk file upload: sandbox.writeFiles(files) uploads multiple files in one call via the SDK's native fs.uploadFiles().
    • Detached lifecycle: getPortUrl(), stop(), and destroy() now work from a fresh process by looking up the existing sandbox by its id, without starting a stopped sandbox first.
    import { DaytonaSandbox } from '@mastra/daytona';
    
    const sandbox = new DaytonaSandbox({ id: 'my-preview', public: true });
    await sandbox.start();
    const url = await sandbox.networking.getPortUrl(4111);
  • Added clone() support to the sandbox providers. clone() constructs an unstarted sibling sandbox that inherits the template's configuration (credentials, image, resources) with per-instance overrides for id and env, so one configured sandbox can act as a template for a fleet of sandbox clones (for example, one per project). (#19616)

    const template = new E2BSandbox({ apiKey, template: 'base' });
    
    const projectSandbox = template.clone({
      id: 'mc-project-42',
      env: { GITHUB_TOKEN: token },
      idleTimeoutMinutes: 30,
    });
    await projectSandbox.start();

    idleTimeoutMinutes is a best-effort hint that maps to each provider's native lifetime knob (Railway idleTimeoutMinutes, E2B/Modal/Vercel timeout in milliseconds, Daytona autoStopInterval, Blaxel TTL duration). Docker and Apple Container ignore it since they have no provider-side idle teardown.

Patch Changes

@mastra/deployer@1.52.0

Minor Changes

  • Added automatic detection of Software Factory projects when the Mastra entry imports and constructs a MastraFactory binding. Detected projects receive a mastra-project.json manifest in .mastra/output/ identifying the project type and UI asset location. (#19948)

    Use the lightweight public helper when project-type detection is needed before a full bundle:

    import { analyzeEntryProjectType } from '@mastra/deployer/build';
    
    const projectType = await analyzeEntryProjectType('./src/mastra/index.ts');

Patch Changes

  • Fixed Babel 8 compatibility for build-time transforms. (#19393)

  • Fixed "Error: ENOTDIR: not a directory, open '...chunk-XYZ.js/package.json'" errors being printed during mastra build when using custom bundler options without externals: true. Package resolution no longer treats module files as directories when looking up dependency metadata, so builds run without these confusing (but harmless) errors in the output. (#19514)

  • Added secure Agent Learning reads to local Studio development. Requests stay same-origin, and browser-supplied credentials and tenant scope are ignored. (#19759)

    MASTRA_PLATFORM_ACCESS_TOKEN=<organization-api-key> \\
      MASTRA_PROJECT_ID=<project-id> \\
      mastra dev
  • Fixed Mastra development startup with Babel 8. (#19849)

@mastra/deployer-cloudflare@1.2.8

Patch Changes

  • Fixed Babel 8 compatibility for build-time transforms. (#19393)

@mastra/deployer-sandbox@0.1.0

Minor Changes

  • New package: deploy a full Mastra server (including Studio) into any workspace sandbox that supports networking, and get a live public URL in seconds. Works with @mastra/vercel, @mastra/e2b, and @mastra/daytona sandboxes. Built for ephemeral environments: instant previews, PR/CI smoke deploys, agent-built app verification, and multi-tenant untrusted instances. (#19577)

    Deploy from your Mastra config

    import { Mastra } from '@mastra/core/mastra';
    import { SandboxDeployer } from '@mastra/deployer-sandbox';
    import { VercelSandbox } from '@mastra/vercel';
    
    export const mastra = new Mastra({
      deployer: new SandboxDeployer({
        sandbox: new VercelSandbox({ sandboxName: 'my-preview', ports: [4111] }),
      }),
    });

    Then run mastra build — it bundles the project and deploys it into the sandbox in one step. Redeploys reuse the same sandbox and skip dependency installs when the install inputs (package.json, bundled lockfiles, and the install command) are unchanged.

    Manage the deployment

    The sandbox name is the identity — getDeployment() retrieves the deployment from any process or codebase, without importing the Mastra project:

    import { getDeployment } from '@mastra/deployer-sandbox/client';
    import { VercelSandbox } from '@mastra/vercel';
    
    const dep = await getDeployment({
      sandbox: new VercelSandbox({ sandboxName: 'my-preview', ports: [4111] }),
    }); // never wakes a stopped sandbox
    await dep.stop(); // snapshot-stop (resumable)
    await dep.destroy(); // permanent delete

    Deploy programmatically (CI / agents)

    import { deployToSandbox } from '@mastra/deployer-sandbox';
    import { VercelSandbox } from '@mastra/vercel';
    
    const sandbox = new VercelSandbox({ sandboxName: 'ci-preview', ports: [4111] });
    const deployment = await deployToSandbox({ sandbox, dir: '.mastra/output' });
    console.info(deployment.url);

    Resolve and route at runtime

    The server-only @mastra/deployer-sandbox/client export includes getDeployment() to resolve the current URL and manage the deployment (stop(), destroy(), logs(), with optional wake-on-demand), plus createSandboxHandler() and createSandboxProxy() helpers to serve a sandbox behind a stable URL on your own domain.

Patch Changes

@mastra/docker@0.5.0

Minor Changes

  • Added clone() support to the sandbox providers. clone() constructs an unstarted sibling sandbox that inherits the template's configuration (credentials, image, resources) with per-instance overrides for id and env, so one configured sandbox can act as a template for a fleet of sandbox clones (for example, one per project). (#19616)

    const template = new E2BSandbox({ apiKey, template: 'base' });
    
    const projectSandbox = template.clone({
      id: 'mc-project-42',
      env: { GITHUB_TOKEN: token },
      idleTimeoutMinutes: 30,
    });
    await projectSandbox.start();

    idleTimeoutMinutes is a best-effort hint that maps to each provider's native lifetime knob (Railway idleTimeoutMinutes, E2B/Modal/Vercel timeout in milliseconds, Daytona autoStopInterval, Blaxel TTL duration). Docker and Apple Container ignore it since they have no provider-side idle teardown.

Patch Changes

@mastra/dynamodb@1.1.3

Patch Changes

  • Fixed listMessages() silently truncating large threads, returning incorrect pagination metadata, and failing to retrieve included-message context beyond the first DynamoDB page. Large threads now return complete paginated results with correct total and hasMore, and include lookups (e.g. jumping to a message deep in a thread) again return the requested surrounding context instead of partial or empty windows. (#19562)

  • Fixed a TypeScript build error in the DynamoDB store where date range filtering in listMessages failed strict null checks (#19567)

  • Fixed listMessages pagination so hasMore is false when include (e.g. withNextMessages/withPreviousMessages) already returns every message in the queried thread(s). Previously hasMore was derived from offset + perPage < total only, so it stayed true even when the included context completed the full set — matching the behavior of the pg store. (#19371)

  • Fixed listMessages reporting hasMore: false too early when include context messages fall outside the active resourceId or dateRange filters. Only messages matching the filters now count toward the returned total, so later filter-matching pages are no longer hidden. (#19576)

@mastra/e2b@0.7.0

Minor Changes

  • Added networking and bulk file upload support to E2BSandbox: (#19577)

    • Public port URLs: sandbox.networking.getPortUrl(port) returns the public URL for a port, enabling preview URLs and deploys with the new @mastra/deployer-sandbox package.
    • Bulk file upload: sandbox.writeFiles(files) uploads multiple files in one call.
    • Snapshot-stop: stop() now pauses the sandbox immediately (snapshotting filesystem, memory, and running processes) instead of leaving it running until its timeout. The next start() with the same id resumes it, with background processes still running. destroy() still kills the sandbox permanently.
    • Detached lifecycle: getPortUrl(), stop(), and destroy() now work from a fresh process by looking up the existing sandbox by its id metadata, without resuming a paused sandbox. Stopping or destroying never wakes (or bills) a paused sandbox first.
    import { E2BSandbox } from '@mastra/e2b';
    
    const sandbox = new E2BSandbox();
    await sandbox.start();
    const url = await sandbox.networking.getPortUrl(4111);
  • Added clone() support to the sandbox providers. clone() constructs an unstarted sibling sandbox that inherits the template's configuration (credentials, image, resources) with per-instance overrides for id and env, so one configured sandbox can act as a template for a fleet of sandbox clones (for example, one per project). (#19616)

    const template = new E2BSandbox({ apiKey, template: 'base' });
    
    const projectSandbox = template.clone({
      id: 'mc-project-42',
      env: { GITHUB_TOKEN: token },
      idleTimeoutMinutes: 30,
    });
    await projectSandbox.start();

    idleTimeoutMinutes is a best-effort hint that maps to each provider's native lifetime knob (Railway idleTimeoutMinutes, E2B/Modal/Vercel timeout in milliseconds, Daytona autoStopInterval, Blaxel TTL duration). Docker and Apple Container ignore it since they have no provider-side idle teardown.

Patch Changes

@mastra/evals@1.6.0

Minor Changes

  • Added context-recall LLM scorer that evaluates how well retrieved context covers the claims in a ground-truth reference answer. Complements the existing context-precision scorer by measuring retrieval completeness rather than relevance ranking. (#19733)

    import { createContextRecallScorer } from '@mastra/evals/scorers/prebuilt';
    
    const scorer = createContextRecallScorer({
      model: 'openai/gpt-5-mini',
      options: {
        context: [
          'Einstein was born on 14 March 1879 in Ulm, Germany.',
          'Einstein developed the theory of special relativity in 1905.',
        ],
      },
    });

Patch Changes

@mastra/factory@0.1.0

Minor Changes

  • Move the Factory project CRUD and source-control connection routes into @mastra/factory as a ProjectRoutes class. The routes take their storage handles (FactoryProjectsStorage, SourceControlStorage), the allowed version-control integration ids, and a RouteAuth adapter at construction time, replacing the old ProjectDomain that resolved domains through the FactoryStorage registry. The now-unused FactoryDomain base class was removed from the web host. (#19866)

  • Move the audit domain, agent git-action auditing, intake capabilities, and intake routes into @mastra/factory. AuditDomain now takes its storage handles (AuditStorage, FactoryProjectsStorage) and a RouteAuth adapter directly instead of resolving them through the factory storage registry, fans out to pluggable AuditSinks, and resolves agent tenants through an injected agentTenant callback. Intake routes ship as an IntakeRoutes class that calls IntakeStorage directly (the intermediate intake store module was removed). (#19866)

  • Added autonomous first-pass skills to the Software Factory. Work items now get an automatic investigation, planning, or review pass as soon as they enter the matching board column — no human input needed mid-run: (#20058)

    • factory-triage runs when an issue enters triage: it investigates the issue, diagnoses the root cause, and requests a move to planning (or done if the issue should be closed).
    • factory-plan runs when an item enters planning: it produces a phased implementation plan and requests a move to execute.
    • factory-review runs when a pull request enters review: it reviews the changes, posts a verdict, and requests completion.

    Instead of stopping to ask questions, the skills decide and record each decision as an assumption, batching assumptions and genuinely-human questions into one terminal handoff message. The superseded interactive skills (understand-issue, understand-pr) were removed.

  • Move the FactoryIntegration contract and the OAuth state signer into @mastra/factory. The integration interface (routes, tools, diagnostics, intake/version-control capabilities, IntegrationContext) now lives at @mastra/factory/integrations/base, and createStateSigner/StateSigner at @mastra/factory/state-signing, so integrations can be implemented against the package without importing the web host. (#19866)

  • Added the @mastra/factory package. It now owns the Software Factory storage domains (projects, work items, intake, audit, credentials, integrations, model packs, queue health, source control) that previously lived inside the mastracode web app, so they can be reused outside the web server. (#19866)

  • Moved the server config routes and provider credential helpers into @mastra/factory as a reusable ConfigRoutes class. Route handlers now receive their auth checks through an injected RouteAuth seam and storage domains through constructor options, so hosts other than the Mastra Code web app can mount the same routes. (#19866)

  • Move the Factory work-item (kanban board) routes into @mastra/factory as a WorkItemRoutes class. The routes take their storage handles (WorkItemsStorage, FactoryProjectsStorage, QueueHealthStorage), an AuditEmitter, and a RouteAuth adapter at construction time. The request-body validators (parseCreateWorkItem, parseUpdateWorkItem) now live with the routes, the pass-through work-item store module was removed in favor of calling WorkItemsStorage directly, and computeFactoryMetrics takes a single object parameter. (#19866)

Patch Changes

  • Move the WorkOS audit integration into @mastra/factory/integrations/workos. Its Admin Portal route now resolves the caller through the RouteAuth seam on IntegrationContext instead of web-host auth helpers, and @mastra/auth-workos becomes a package dependency. (#19866)

  • Move the factory auth module into @mastra/factory/auth. The provider-neutral (#19866)
    auth gating (mountFactoryAuth, buildAuthRoutes, createFactoryAuthGate),
    the RouteAuth implementation (createFactoryRouteAuth), and the WorkOS/SSO
    helpers now live next to the route seam they implement, with factory naming
    throughout.

  • The Factory's default publicUrl is now http://localhost:4111 (the Factory server, which serves both the UI and the API) instead of http://localhost:5173. Generated Factory projects now run from a single server, so OAuth callback URLs and auth redirects derived from publicUrl point at the right origin out of the box. If you serve the SPA from a separate origin (for example a Vite dev server on :5173), set publicUrl (or MASTRACODE_PUBLIC_URL) explicitly. (#20036)

  • Factory board now picks up new GitHub/Linear intake automatically (gentle 30s poll) and refreshes work-item positions immediately when the tab regains focus, instead of requiring a manual page reload (#20071)

  • Fixed GitHub PATs saved in Settings not taking effect for the gh CLI in already-running Factory sessions until the server was restarted (#20069)

  • Forwarded closed Platform GitHub event-log deliveries into Factory governance before dispatching repository subscriptions, and kept default GitHub rules from auto-starting issues or pull requests created before the Factory. (#19988)

  • Track per-stage automation in Factory metrics. Stage history now stamps the exiting actor (exitedBy) alongside the entering one, isAutomationActor classifies rules-engine, agent (agent:*), and webhook (github:*) actors as automation, and computeFactoryMetrics reports a stageAutomation breakdown per stage: how many passes were fully automated (entered and exited by automation on the first visit) and how those automated passes ended up (done, canceled, reworked, or still in flight). Adds the canceled terminal stage to the board vocabulary (FACTORY_RULE_STAGES) — a tracked non-completion that feeds neither throughput nor cycle time — and rewords organization-required errors to be auth-provider neutral. (#19844)

  • Fixed @mastra/factory build output so published modules use explicit .js import extensions and resolve correctly under Node ESM (#19954)

  • Deployed factories now authenticate API and Studio requests with the same provider, so Studio sessions work without extra configuration. (#19966)

  • Fixed Factory metrics windowing to use inclusive UTC calendar days. Date-only from/to bounds now include both selected days, an item completing at the current instant is counted in today's throughput (previously it could be dropped on the window's exclusive edge), and windowDays reflects the number of gap-filled day buckets. Cards feed the source mix only when created inside the window. (#19971)

  • Fixed duplicate repositories in Factory source control settings. (#19971)

  • Move the API-surface assembler from mastracode/web into @mastra/factory as routes/surfaceassembleWebApiRoutes is now assembleFactoryApiRoutes and WebApiRoutesDeps is now FactoryApiRoutesDeps. The module composes fs/config/oauth/skills/intake/work-item routes plus every registered integration's route surface (with disabled-status stubs for absent github/linear integrations) from explicitly threaded dependency handles. (#19866)

  • Move the GitHub integration and the sandbox fleet into @mastra/factory. The fleet is now a DI-constructed SandboxFleet class (@mastra/factory/sandbox/fleet) that owns provisioning, reattach, teardown, idle windows, and per-replica budgets instead of reading a seeded runtime-config registry. The GitHub routes, webhook, sandbox materialization, project locks, and session subscriptions (@mastra/factory/integrations/github) resolve tenants through the RouteAuth seam and receive the fleet and factory storage via IntegrationContext, so the web host no longer exports getSeededSandbox/getSeededGithubIntegration service locators. (#19866)

  • Move the filesystem routes (@mastra/factory/routes/fs) and skill routes (@mastra/factory/routes/skills) into @mastra/factory. The skill prepare/invoke routes are now a SkillRoutes class that resolves users and tenants through the RouteAuth seam instead of web-host auth helpers. Diagnostics fields exposed by the GitHub and Linear integrations rename webAuthEnabled to factoryAuthEnabled to match the package's auth seam naming. (#19866)

  • Moved custom model providers and custom model packs off settings.json in the factory web app: both now live in the app database (org-scoped rows in deployed mode, a sentinel local scope in no-auth mode). Custom providers saved in the web settings page are picked up by model resolution and the model catalog through a new pluggable custom-providers source in the SDK, so the gateway no longer reads the host machine's settings.json for them, and models from your custom providers appear in the web model pickers. (#19964)

    Hosts that store custom providers elsewhere (like the factory's database) register a source at boot; when none is registered, the SDK keeps reading settings.json as before:

    import { setCustomProvidersSource } from '@mastra/code-sdk/agents/custom-provider-source';
    
    setCustomProvidersSource(tenant => (tenant ? snapshotForOrg(tenant.orgId) : []));
  • Fixed cloned session threads reading from a previous storage instance. The dynamic memory cache now invalidates when the storage or vector instance changes, so thread cloning always uses the current database. (#19966)

  • Added a memory-settings storage domain: observational memory settings (observer and reflector models, thresholds, attachment observation) changed in the web app are now stored in the app database — one row per user — instead of settings.json, and the settings page reads them back from the database. Factory-mounted agent controllers no longer seed observational memory settings from the host machine's settings.json (new disableSettingsOmSeed SDK option), so server sessions start from built-in defaults plus whatever is stored in the database. The OM settings model pickers in the web UI are now searchable comboboxes. (#19964)

    Server embedders that persist memory settings in their own database can opt out of the settings.json seed:

    import { createMastraCode } from '@mastra/code-sdk';
    
    const mastraCode = await createMastraCode({
      cwd: process.cwd(),
      // Don't seed observer/reflector models or thresholds from the host
      // machine's settings.json — sessions start from built-in defaults.
      disableSettingsOmSeed: true,
    });
  • Move the Linear integration into @mastra/factory/integrations/linear. LinearIntegration now owns the full connection lifecycle (OAuth token exchange, single-flight refresh, scope checks, and connection caching) as class methods, the routes and agent tools resolve tenants through the RouteAuth seam instead of web-host auth imports, and the getSeededIntegration runtime-config indirection is gone — the host hands the integration instance and storage handles directly via initialize(). (#19866)

  • Fixed Factory automation so polled GitHub events reach governance rules, authenticated sessions start with the correct ownership, and board moves reliably notify active or idle agents. (#19979)

  • Move the MastraFactory assembly root into @mastra/factory. factory-entry.ts now lives at the package root export (@mastra/factory), alongside the extracted workspace, spa-static, server-error, and sandbox/reattach helpers. Factory skills ship with the package and are copied into deploy output via the consuming app's build script. (#19866)

  • Fixed web chat sessions getting stuck in a "Connection lost — reconnecting…" loop while the session workspace was still starting up (#20067)

  • Fixed a server startup crash when the factory's storage backend could not be recognized by the SDK. The factory now tells the SDK explicitly whether its Mastra store is Postgres or LibSQL, so agent state wiring works even when the project's dependency graph contains duplicate copies of Mastra packages. (#20030)

@mastra/hono@1.5.8

Patch Changes

  • Fixed agent replies from channels being silently dropped on Cloudflare Workers. (#19299)

    The bot would receive a message and the webhook returned 200 OK, but no reply was posted and nothing was logged. Channel replies run after the webhook responds, and on Cloudflare Workers that remaining work was being dropped once the response was sent. It is now kept running to completion, so the reply is delivered. (#19285)

@mastra/inngest@1.8.3

Patch Changes

  • Fixed Inngest TypeScript build failures for workflows whose steps use typed request context. (#19225)

  • Fixed nested Inngest workflows to resume the suspended child step when only the parent workflow step is provided. (#19752)

@mastra/libsql@1.17.0

Minor Changes

  • Added LibSQLFactoryStorage for persisting Mastra agent state and lifecycle-managed application domains through one LibSQL connection. (#19681)

    import { LibSQLFactoryStorage } from '@mastra/libsql';
    
    const storage = new LibSQLFactoryStorage({ url: 'file:mastra.db' });
    await storage.init();

Patch Changes

  • Replaced GitHub-specific Mastra Code session state with Factory project and linked-repository identities. This lets SDK consumers represent sessions independently of a source-control provider and select a repository explicitly when sandbox execution is required. (#19849)

    Updated Mastra Code onboarding to be Factory-first: create a Factory by name, then link repositories from your connected source-control installations in a separate step. A Factory is valid with zero linked repositories, and the Board, Metrics, and Audit pages stay available for any server-backed Factory. Factory pages keep project-scoped data separate from repository-scoped intake and provide a repository selector when a Factory has multiple linked repositories. Creating a Factory from a local folder remains available as a secondary option.

    Before

    const state = { githubProjectId: 'project-1', sandboxId, sandboxWorkdir };

    After

    const state = {
      factoryProjectId: 'factory-project-1',
      projectRepositoryId: 'project-repository-1',
      sandboxId,
      sandboxWorkdir,
    };
  • Fixed an uncaught RangeError: Maximum call stack size exceeded when writing deeply-nested values (such as a long chained Error.cause) to LibSQL storage. safeStringify now caps its recursion depth and substitutes a "[Max depth exceeded]" sentinel instead of overflowing the stack. (#19610)

  • Fix factory storage error classification and schema drift handling. (#20002)

    • isUniqueViolation in the LibSQL factory storage now only matches real unique-index violations (SQLITE_CONSTRAINT_UNIQUE / SQLITE_CONSTRAINT_PRIMARYKEY). Previously any SQLITE_CONSTRAINT failure — including NOT NULL violations — was reported as a "Unique constraint violation", which masked the real error (seen as Unique constraint violation on collection 'factory_rule_evaluations' when polled ingestion inserted nullable columns into a stale table).
    • ensureCollections in both the LibSQL and Postgres factory storage adapters now relaxes stale NOT NULL constraints when a column becomes nullable in the schema. Postgres uses ALTER COLUMN ... DROP NOT NULL; LibSQL rebuilds the table in place since SQLite cannot drop NOT NULL directly. Existing rows and unique indexes are preserved.

@mastra/mcp@1.15.0

Minor Changes

  • Complete the OAuth authorization-code loop in MCPClient for HTTP MCP servers. Connections rejected with an authorization error now surface a needs-auth state (readable via getServerAuthState()), and the new authenticate(serverName) method runs the interactive flow end to end: it captures the authorization code on a local loopback callback server (exported as createOAuthCallbackServer for hosts with custom redirect handling), exchanges it for tokens, and reconnects. (#19466)

Patch Changes

  • Fixed MCP clients getting stuck after a failed reconnect to streamable-HTTP-only servers. listTools(), callTool(), and forceReconnect() now work once the server is reachable again. Fixes #19862 (#19894)

@mastra/memory@1.23.1

Patch Changes

  • Fixed observational memory aborting the agent run when OpenRouter injects a transient provider error into the response stream (e.g. "JSON error injected into SSE stream" with google/gemini-2.5-flash). These mid-stream errors carry the HTTP status on a numeric code property and are now recognized as retryable, so the observer retries with backoff instead of failing the turn. (#19640)

  • Updated the async-buffering compatibility error to include @mastra/convex in the list of storage adapters that support Observational Memory. (#19474)

@mastra/modal@0.4.0

Minor Changes

  • Added clone() support to the sandbox providers. clone() constructs an unstarted sibling sandbox that inherits the template's configuration (credentials, image, resources) with per-instance overrides for id and env, so one configured sandbox can act as a template for a fleet of sandbox clones (for example, one per project). (#19616)

    const template = new E2BSandbox({ apiKey, template: 'base' });
    
    const projectSandbox = template.clone({
      id: 'mc-project-42',
      env: { GITHUB_TOKEN: token },
      idleTimeoutMinutes: 30,
    });
    await projectSandbox.start();

    idleTimeoutMinutes is a best-effort hint that maps to each provider's native lifetime knob (Railway idleTimeoutMinutes, E2B/Modal/Vercel timeout in milliseconds, Daytona autoStopInterval, Blaxel TTL duration). Docker and Apple Container ignore it since they have no provider-side idle teardown.

Patch Changes

@mastra/mongodb@1.14.0

Minor Changes

  • Added a filterFields option to MongoDBVector.createIndex(). Declaring the metadata fields you filter on lets Mastra register them as native filter fields in the Atlas vectorSearch index, so filtered queries are pushed straight into $vectorSearch instead of first materialising matching document _ids. This removes the 16 MB BSON ceiling that previously capped metadata-filtered queries at roughly 342,000 matching documents. (#19006)

    await vectorStore.createIndex({
      indexName: 'my-index',
      dimension: 1536,
      metric: 'cosine',
      filterFields: ['category', 'tenant_id'],
    });

    Queries that filter only on declared fields (using operators Atlas Vector Search supports) take the fast path automatically. Filters that reference an undeclared field, or use an unsupported operator, keep working through the existing pre-filter.

Patch Changes

  • Fixed MongoDBVector.createIndex() leaving index setup incomplete. Previously, when the vector search index already existed, the call silently skipped creating the companion full-text search index. Repeated or interrupted createIndex() calls now finish creating the remaining search index instead of leaving setup incomplete. (#19006)

@mastra/observability@1.16.2

Patch Changes

  • Fixed cost estimates for recently added provider models, cached input tokens, and long-context requests. (#19830)

  • Fixed cost estimates when AI SDK provider names differ from embedded pricing data, including Vercel AI Gateway and Google Vertex. (#19513)

@mastra/pg@1.17.0

Minor Changes

  • Added PgFactoryStorage for persisting Mastra agent state and lifecycle-managed application domains through one PostgreSQL pool. (#19681)

    import { PgFactoryStorage } from '@mastra/pg';
    
    const storage = new PgFactoryStorage({ connectionString: process.env.DATABASE_URL! });
    await storage.init();

Patch Changes

  • Fixed a missing semicolon in the SQL generated by exportSchemas(). The CREATE INDEX statement for the favorites table ran into the next CREATE TABLE statement, so applying the exported schema as a single query (for example with pg's client.query() or a Supabase migration) failed with a Postgres syntax error. Fixes #19942 (#19944)

  • Replaced GitHub-specific Mastra Code session state with Factory project and linked-repository identities. This lets SDK consumers represent sessions independently of a source-control provider and select a repository explicitly when sandbox execution is required. (#19849)

    Updated Mastra Code onboarding to be Factory-first: create a Factory by name, then link repositories from your connected source-control installations in a separate step. A Factory is valid with zero linked repositories, and the Board, Metrics, and Audit pages stay available for any server-backed Factory. Factory pages keep project-scoped data separate from repository-scoped intake and provide a repository selector when a Factory has multiple linked repositories. Creating a Factory from a local folder remains available as a secondary option.

    Before

    const state = { githubProjectId: 'project-1', sandboxId, sandboxWorkdir };

    After

    const state = {
      factoryProjectId: 'factory-project-1',
      projectRepositoryId: 'project-repository-1',
      sandboxId,
      sandboxWorkdir,
    };
  • Fixed an issue where the process could crash when Postgres drops an idle database connection (e.g., due to a backend restart, failover, or network failure). (#19469)

    Previously, a dropped idle connection caused an uncaught exception that terminated the process. Now the connection drop is caught and logged as a warning, and the store automatically reconnects on the next database operation.

    User-provided connection pools are unaffected and keep their existing error handling.

  • Fix factory storage error classification and schema drift handling. (#20002)

    • isUniqueViolation in the LibSQL factory storage now only matches real unique-index violations (SQLITE_CONSTRAINT_UNIQUE / SQLITE_CONSTRAINT_PRIMARYKEY). Previously any SQLITE_CONSTRAINT failure — including NOT NULL violations — was reported as a "Unique constraint violation", which masked the real error (seen as Unique constraint violation on collection 'factory_rule_evaluations' when polled ingestion inserted nullable columns into a stale table).
    • ensureCollections in both the LibSQL and Postgres factory storage adapters now relaxes stale NOT NULL constraints when a column becomes nullable in the schema. Postgres uses ALTER COLUMN ... DROP NOT NULL; LibSQL rebuilds the table in place since SQLite cannot drop NOT NULL directly. Existing rows and unique indexes are preserved.
  • Added stable metrics and logs capability reporting for observability storage. The system packages response now includes observabilityStorageCapabilities with metrics and logs flags, enabling capability-based detection that is resilient to bundler-generated constructor name changes. (#19305)

    const packages = await client.getSystemPackages();
    console.log(packages.observabilityStorageCapabilities?.metrics); // true
    console.log(packages.observabilityStorageCapabilities?.logs); // true

    Studio now uses the capability response instead of relying on constructor names, with a fallback for older servers.

@mastra/platform-workspace@0.2.0

Minor Changes

  • Renamed the environment variable read by PlatformSandbox and PlatformFilesystem for platform authentication from MASTRA_PLATFORM_ACCESS_TOKEN to MASTRA_PLATFORM_SECRET_KEY. The old variable still works as a deprecated fallback. (#19932)

  • Added clone() support to PlatformSandbox. clone() constructs an unstarted sibling sandbox that inherits the template's configuration (access token, project, environment, network isolation, timeout, instructions, env, idle timeout) with per-instance overrides for id, sandboxId, env, and idleTimeoutMinutes, so one configured sandbox can act as a template for a fleet of sandbox clones (for example, one per project). (#19647)

    const template = new PlatformSandbox({
      accessToken,
      projectId,
      environmentId,
    });
    
    const projectSandbox = template.clone({
      id: 'mc-project-42',
      env: { GITHUB_TOKEN: token },
      idleTimeoutMinutes: 30,
    });
    await projectSandbox.start();

    This brings PlatformSandbox up to parity with the other sandbox providers (@mastra/railway, @mastra/e2b, @mastra/daytona, @mastra/modal, @mastra/docker, @mastra/blaxel, @mastra/apple-container, @mastra/vercel) so it can be used with MastraFactory fleets and the MC Web factory.

Patch Changes

  • PlatformSandbox now includes its caller-facing id on the POST /v1/projects/:projectId/sandbox wire body when provisioning a new sandbox. The Mastra Platform treats this as an advisory recovery key so callers can opt into checkpoint-based sandbox recovery — if the platform recognizes the id from a previous session, the new sandbox boots from the most recent checkpoint instead of the base template. Unknown ids fall through to a fresh sandbox, so existing callers see no change in behavior. (#19648)

    No API changes — the value sent is the same id you already pass to new PlatformSandbox({ id }) (or the auto-generated one).

@mastra/playground-ui@42.0.0

Minor Changes

  • Added ClampedText, a design-system component that clamps text to a number of lines and shows a "Read more" toggle only when the clamp actually cuts content. Detection is based on the rendered layout (element measurement, re-checked on resize and after fonts load), not on character count. (#19565)

  • Added reusable Ask User and Task List components for agent interfaces. (#19624)

  • Added separate Sankey node identity, display label, display value, and layout weight accessors, plus constrained labels with full hover text. Stable layout weights can now keep node positions fixed while current record weights animate bar and ribbon sizes. (#19871)

    <Sankey
      data={records}
      columns={columns}
      getRecordNodeId={(record, column) => String(record[`${column.id}Id`])}
      getRecordNodeLabel={(record, column) => String(record[`${column.id}Label`])}
      getRecordNodeValue={(record, column) => Number(record[`${column.id}Count`])}
      getRecordWeight={record => Number(record.count)}
      getRecordLayoutWeight={record => Number(record.windowMaxCount)}
    >
      <SankeyChart />
    </Sankey>
  • Added compact sizing, custom interactive elements, and trailing actions to sidebar navigation items. (#19959)

    <MainSidebar.NavLink
      // Compact density for dense lists
      size="sm"
      // Bring your own interactive element (e.g. a router Link or button)
      render={<Link to="/sessions/feature-work">Feature work</Link>}
      // Trailing control rendered beside the row, independently clickable
      action={
        <Button size="icon-sm" variant="ghost" onClick={onDelete}>
          <TrashIcon />
        </Button>
      }
    />
  • Added optional Sankey node activation with mouse and keyboard support, including per-node eligibility. (#19896)

    <SankeyChart
      onNodeClick={({ column, value }) => openDrilldown(column.id, value)}
      isNodeClickable={({ value }) => drillableNodeIds.has(value)}
    />
  • Added a revealScrollbarOnHover prop to ScrollArea. Set it to false to keep the overlay scrollbar hidden until the user actively scrolls, instead of also revealing it when the pointer hovers the area. Defaults to true, so existing usage is unchanged. (#20037)

    <ScrollArea revealScrollbarOnHover={false}>{content}</ScrollArea>
  • Added a borderless surface appearance, compact content density, and a dedicated card link component with native link semantics, plus hover, focus, and active states for interactive cards. (#19747)

  • Added a composable Sankey chart with deterministic colors, gradient ribbons, connected hover feedback, percentage labels, configurable margins, and opt-in column palettes. Redesigned Signals around Agent Learning theme-flow analysis with weighted graph metrics, shared signal colors, compact distribution summaries, and a guided trace-to-signal onboarding pipeline when usable data is not yet available. Fixed the empty state to match Sankey colors and emphasize the Mastra Engine stage. (#19472)

Patch Changes

  • Added a disabled prop to DataList.SelectCell so rows can block checkbox toggling while an async selection save is pending. (#20038)

  • Fixed the ClampedText read-more button to announce its expanded state to screen readers, and fixed clamp measurement so font-load re-measure and effect cleanup still run in browsers without ResizeObserver. (#19582)

  • Fixed composer inputs to show one scrollbar for long messages. Added a maxHeight prop for configuring the scrolling viewport. (#19876)

  • Exported the shared toast API through the Toaster component entry point. (#19876)

  • Improved breadcrumb navigation with clearer hover and pressed feedback for clickable path segments. (#19840)

  • Added reusable Composer compound components for building controlled chat inputs. (#19470)

  • Fixed named form fields adding a second page scrollbar inside app scroll containers. (#19617)

  • Improved sidebar density and prevented date range timelines from clipping edge labels. (#19972)

  • Removed interaction hints from the date range timeline. (#19987)

  • Fixed hidden search labels so search fields are announced correctly by assistive technologies. (#19840)

@mastra/posthog@1.2.0

Minor Changes

  • Added feedback export to the PostHog observability exporter. Feedback recorded with addFeedback() now flows to PostHog as native $ai_feedback events and appears as "User feedback" on the linked trace, alongside the traces the exporter already sends. No configuration changes are needed. (#19922)

    const trace = await mastra.observability.getRecordedTrace({ traceId });
    await trace.addFeedback({
      feedbackType: 'thumbs',
      value: 'down',
      comment: 'Wrong answer',
    });
    // The PostHog exporter now forwards this as a $ai_feedback event

    Fixes #19893

Patch Changes

@mastra/rag@2.4.2

Patch Changes

  • Added a typed turbopuffer entry to the databaseConfig option of createVectorQueryTool. This lets you set the Turbopuffer consistency level for vector queries. (#19627)

    const vectorTool = createVectorQueryTool({
      vectorStoreName: 'turbopuffer',
      indexName: 'documents',
      model: embedModel,
      databaseConfig: {
        turbopuffer: {
          consistency: 'eventual', // lower latency, recently written data may not be visible yet
        },
      },
    });

@mastra/railway@0.4.0

Minor Changes

  • Added clone() support to the sandbox providers. clone() constructs an unstarted sibling sandbox that inherits the template's configuration (credentials, image, resources) with per-instance overrides for id and env, so one configured sandbox can act as a template for a fleet of sandbox clones (for example, one per project). (#19616)

    const template = new E2BSandbox({ apiKey, template: 'base' });
    
    const projectSandbox = template.clone({
      id: 'mc-project-42',
      env: { GITHUB_TOKEN: token },
      idleTimeoutMinutes: 30,
    });
    await projectSandbox.start();

    idleTimeoutMinutes is a best-effort hint that maps to each provider's native lifetime knob (Railway idleTimeoutMinutes, E2B/Modal/Vercel timeout in milliseconds, Daytona autoStopInterval, Blaxel TTL duration). Docker and Apple Container ignore it since they have no provider-side idle teardown.

Patch Changes

  • Allowed derived Railway sandboxes to override the checkpoint name used for checkpoint-backed creation and refresh. (#19907)

@mastra/react@1.3.0

Minor Changes

  • Added request-scoped model overrides to agent execution and approvals, and fixed Studio model selection so each tab can use a different model without changing the agent's configured default. (#19749)

    await agent.stream(messages, { model: 'google/gemini-2.5-flash' });

Patch Changes

  • Fixed binary file attachments missing during live streaming in useChat when enableThreadSignals is enabled. Attachments sent as Uint8Array or ArrayBuffer now render in the optimistic pending bubble and stay visible after the signal echo, without requiring a page refresh. (#19439)

  • Fix type errors in useChat send-message flow after generated route body types tightened requestContext from any to Record<string, unknown>. (#19573)

@mastra/server@1.52.0

Minor Changes

  • Added request-scoped model overrides to agent execution and approvals, and fixed Studio model selection so each tab can use a different model without changing the agent's configured default. (#19749)

    await agent.stream(messages, { model: 'google/gemini-2.5-flash' });
  • Added exact thread binding to the AgentController session creation endpoint. (#19902)

Patch Changes

  • Fixed agent generate and stream requests being rejected with a 400 error when memory.resource was omitted but server auth was configured with mapUserToResourceId. The request body schema required memory.resource even though the server derives the resource ID from the authenticated user and overrides any client-provided value. (#19524)

    Clients no longer need to send a placeholder resource ID:

    {
      "messages": ["what was my last message?"],
      "memory": { "thread": "test-thread" }
    }

    If a request uses memory and neither the body nor the authenticated request context provides a resource ID, the server now returns a clear 400 error. Fixes #19518.

  • Prevented redundant agent controller thread switches from interrupting active responses. (#19333)

  • Fixed agent replies from channels being silently dropped on Cloudflare Workers. (#19299)

    The bot would receive a message and the webhook returned 200 OK, but no reply was posted and nothing was logged. Channel replies run after the webhook responds, and on Cloudflare Workers that remaining work was being dropped once the response was sent. It is now kept running to completion, so the reply is delivered. (#19285)

  • Fixed durable-agent check in recover route (#19632)

  • Fixed agent controller session routes to use the active request context when creating or resolving sessions. (#19642)

  • Fixed dataset item endpoints to return HTTP 400 when payloads contain circular values, silently lossy JSON values (nested undefined, functions, symbols, bigints, and non-finite numbers), or non-plain objects (Date, Map, Set, class instances, and custom toJSON() objects) that cannot be serialized faithfully. Also fixed the add/update dataset item endpoints to persist the caller-provided requestContext entries instead of the live server request context instance, which contained internal server state and could fail storage serialization. (#19603)

  • Documented the optional requestContext body field on agent controller run routes (send message, steer, follow-up, tool approval, tool suspension) so it appears in the generated OpenAPI spec. The server already merged this field into the request context; only the route schemas were missing it. (#19531)

  • Replace any with unknown in generated route types so clients get real type errors instead of silently unchecked values. Route-types generation now deduplicates shared schemas into reusable type aliases, shrinking the generated route-types.generated.ts from ~94K to ~22K lines (77% smaller). The memory config response now types workingMemory (enabled, scope, template, schema, version) instead of unknown. (#19573)

    If your code read fields from responses that were previously typed any, TypeScript now requires you to narrow them before use:

    // Before: `metadata` was `any`, so this compiled even when unsafe
    const value = response.metadata.someField;
    
    // After: `metadata` is `unknown` — narrow it first
    const metadata = response.metadata;
    if (metadata && typeof metadata === 'object' && 'someField' in metadata) {
      const value = metadata.someField;
    }
    // ...or cast if you know the shape: (metadata as MyMetadata).someField

    Code that reads workingMemory from the memory config response no longer needs casts — `config.workingMemory?

Don't miss a new mastra release

NewReleases is sending notifications on new releases.