github mastra-ai/mastra @mastra/core@1.77.0
October 14, 2026

5 hours ago

Highlights

Factory Sandbox Provider Contract + New Providers (Docker/E2B/Platform)

Mastra Factory now has a first-class FactorySandbox contract for sandbox providers, enabling tunable settings schemas, repository templates, and prebuilt environment builds. New *FactorySandbox implementations ship for Docker (@mastra/docker), E2B (@mastra/e2b), and Mastra Platform (@mastra/platform-workspace) to plug in with a single provider instance.

Factory Environment Builds + Multi-Repo Sessions

Factory can now build environment templates ahead of sessions (manual, scheduled cron, or push-triggered builds) when the sandbox supports builds, reducing first-session cold starts. Sessions also boot every repository in the environment (ordered, shared workspace root) and can push/open change requests across any environment repo via source_control_* tools with a repository slug.

Durable Agent Safety: Run Fencing Across Storage Backends

Multiple storage adapters (Cloudflare DO/D1, Convex, DynamoDB, DSQL, MongoDB, MSSQL, MySQL, OracleDB, Spanner, Redis/Upstash/Valkey) now support “run fencing” so a durable execution that lost ownership (e.g., crash recovery) can’t overwrite the recovered run’s threads/messages/workflow state—significantly improving correctness in distributed deployments.

Observational Memory Concurrency + New Conflict-Aware Storage Writes

A large set of fixes harden Observational Memory against overlapping buffering/activation/reflection across processes, adding “generation” protection (supersededBy) and new write-result APIs like commitActiveObservations({ applied, reason }) and appendBufferedObservations({ persisted, recordId }). Observational memory also blocks less often (blockAfter band), preserves correct ordering, and improves reflection fidelity for long-running threads.

Trace Linking & Cross-System Observability (Spans + MCP)

Spans now support links (including W3C traceparent helpers) and exporters propagate them (Datadog/Sentry/OTel bridge/exporter), enabling correlation across traces without joining them. @mastra/mcp additionally links client tool-call spans to server request spans bidirectionally, making it easy to navigate between agent traces and MCP server traces.

Breaking Changes

  • Factory projects now support provider-instance sandboxes (MastraFactoryConfig.sandbox) for settings/builds; existing callback form still works but lacks these capabilities.
  • @mastra/server workflow HTTP routes no longer allow starting/resuming internal workflows (returns 404); durable agents must be invoked via agent routes (e.g., POST /agents/:agentId/stream).
  • AgentController thread/session behavior changes include new suspension “address” claiming APIs and deprecations: SessionRecord.modeId/modelId/state/pending, HarnessStorage.appendPendingItem/updatePendingItem/removePendingItem, and SessionSuspensions.resolveToolCallId().
  • @mastra/playground-ui: Dialog/AlertDialog removed handle/render-style props; components are now controlled via open/onOpenChange.
  • @mastra/client-js / @mastra/react compare-experiments APIs deprecate experimentIdA/experimentIdB in favor of experimentIds (+ optional baselineId).

Changelog

@mastra/core@1.77.0

Minor Changes

  • AgentController steer and followUp now accept file attachments, matching sendMessage. Attached files reach the model the same way whether the message is sent right away or queued behind an active run. (#26909)

    await session.followUp({
      content: 'Then compare against this screenshot.',
      files: [{ data: screenshotBase64, mediaType: 'image/png', filename: 'expected.png' }],
    });

    The /steer and /follow-up routes accept the same files field and limits as /messages.

    Also in: @mastra/client-js@1.54.0, @mastra/server@1.77.0

  • Added FactorySandbox, the contract a host implements to plug a sandbox provider into Mastra Factory. The bare callback that builds one MastraSandbox per session still works, but it gives a provider no way to expose tunable settings, a prebuilt repository template or template builds. A FactorySandbox owns all four, and lives in @mastra/core/workspace so provider packages implement it without depending on @mastra/factory. (#26467)

    Required: provider and create, which returns the session's MastraSandbox.

    Optional: settings, a schema of what a user can tune (zod, JSON Schema or Standard Schema; every field optional, absent means the provider default; no settings when omitted); template returns the environment's repository template for a context and settings; builds starts, reads and lists template builds ahead of any session.

    import { FactorySandbox } from '@mastra/core/workspace';
    import type { FactorySandboxContext } from '@mastra/core/workspace';
    import type { PublicSchema } from '@mastra/core/schema';
    
    interface Settings {
      region?: 'us' | 'eu';
    }
    
    class MyFactorySandbox extends FactorySandbox<Settings> {
      readonly provider = 'my-cloud';
      readonly settings: PublicSchema<Settings> = {
        type: 'object',
        properties: { region: { type: 'string', enum: ['us', 'eu'], default: 'us' } },
        additionalProperties: false,
      };
    
      create(ctx: FactorySandboxContext, settings: Settings) {
        return new MyCloudSandbox({
          id: ctx.sessionId,
          sandboxId: ctx.sandboxId,
          template: this.template(ctx, settings),
          workingDirectory: ctx.workingDirectory,
          region: settings.region ?? 'us',
        });
      }
    
      // Optional: the sandbox template image Factory can build ahead of time
      template(ctx: FactorySandboxContext, settings: Settings) {
        return myCloudTemplate(ctx, settings);
      }
    
      // Optional: build that template ahead of time and report on it
      readonly builds = {
        start: async (ctx: FactorySandboxContext, settings: Settings) => myCloud.startBuild(this.template(ctx, settings)),
        get: async (ctx: FactorySandboxContext, settings: Settings, buildId: string) => myCloud.buildStatus(buildId),
        list: async (ctx: FactorySandboxContext, settings: Settings) => myCloud.builds(this.template(ctx, settings).name),
      };
    }
  • Added authenticated message authors to AgentController sessions. Pass author to sendMessage, queueMessage, or steer, and read it back with getMessageAuthor. (#26549)

    await session.sendMessage({ content: 'Hi', author: { id: 'user-123' } });
    const messages = await session.thread.listMessages({ threadId: session.thread.requireId() });
    const message = messages.findLast(message => message.role === 'signal')!;
    const author = getMessageAuthor(message);
  • Added an optional pricing field to gateway provider configs. A custom gateway can now state what its models cost, in USD per 1 million tokens with the same keys as models.dev, so calls to private or reseller models get cost estimates. (#26875)

    async fetchProviders() {
      return {
        'my-provider': {
          name: 'My Provider',
          models: ['model-1'],
          apiKeyEnvVar: 'MY_API_KEY',
          gateway: 'private',
          url: 'https://api.myprovider.com/v1',
          pricing: { 'model-1': { input: 0.5, output: 1.5 } },
        },
      };
    }
  • Added links to spans: references to spans in other traces. Pass links when starting a span to relate it to a span in another trace without joining that trace. Mastra storage saves the links with the span. (#26059)

    const span = getOrCreateSpan({
      type: SpanType.GENERIC,
      name: 'handle request',
      links: [{ traceId: callerTraceId, spanId: callerSpanId }],
      mastra,
    });

    Also added parseTraceparent() and formatTraceparent() to @mastra/core/observability for W3C traceparent values.

  • Fixed observational memory losing context when a reflection, a buffered-observation write, and an observation activation overlap. The in-memory store now keeps buffered observations across a reflection, never lets activation drop a chunk written by someone else or move the observation cursor backward, and refuses observational memory lifecycle writes (buffered-observation appends, activation, active-observation commits, reflections) to a superseded generation. (#25906)

    Storage gained two methods that report the outcome of a write, plus optional inputs, so callers can detect these cases:

    • commitActiveObservations writes active observations and returns { applied, reason }. It accepts expectedActiveObservations and never writes to a superseded generation.
    • appendBufferedObservations appends a buffered chunk to the current generation and returns { persisted, recordId }.
    • createReflectionGeneration and swapBufferedReflectionToActive accept newRecordId; the reflection applied when the returned record has that id. swapBufferedToActive reports retired.
    • Observational memory records carry supersededBy, set when a newer generation replaces them.

    Before:

    const memory = await storage.getStore('memory');
    await memory.updateActiveObservations({
      id: record.id,
      observations,
      tokenCount,
      lastObservedAt: new Date(),
    });

    After:

    const memory = await storage.getStore('memory');
    const result = await memory.commitActiveObservations({
      id: record.id,
      observations,
      tokenCount,
      lastObservedAt: new Date(),
      expectedActiveObservations: record.activeObservations,
    });
    
    if (!result.applied) {
      // 'retired': a newer generation replaced this record. 'conflict': another writer changed it first.
      // Read the current record and observe again.
    }

    updateActiveObservations and updateBufferedObservations keep returning Promise<void>. In stores that implement the new methods, updateActiveObservations now throws when the observations were not written (the generation was superseded, or expectedActiveObservations no longer matches) instead of resolving as if they had been. An older @mastra/memory running against an upgraded store therefore reports a failed observation and observes the same messages again next turn, rather than marking them observed without saving their observations.

    Storage adapters that do not implement these keep their previous behavior. Upgrade @mastra/core, @mastra/memory, and your storage adapter together to get the full fix; every process writing to the same database needs the upgraded adapter.

  • Added transferable AgentController thread ownership and a per-run owner snapshot in request context. New threads record their owner and creator, while ownership changes to the active thread become visible on the next run. (#26549)

    await session.thread.transferOwnership({ toOwnerId: 'owner-b' });

Patch Changes

  • Fixed aborted agent streams reporting empty token usage. When a stream is aborted, onFinish now receives the usage of steps that completed before the abort: totalUsage is the sum across those steps and usage is the last completed step. Observability and cost tracking for cancelled runs now include that completed-step usage. Usage from the step that was still running when the abort happened is not reported. If no step completed before the abort, usage and totalUsage still have undefined token counts. (#26629)

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

  • Added an optional project_id2 to server feature and model-usage telemetry while preserving existing project identifiers and usage cursors. UUID-shaped MASTRA_PROJECT_ID values are sent unhashed with an mp_ prefix for direct Platform lookup. Other values fall back to a hash of the normalized Git origin remote, discovered asynchronously without blocking startup. The field is omitted when neither source is available. (#22067)

    These events are project-linked, not anonymous. Git-derived identifiers can also be matched against known repository URLs. Set MASTRA_TELEMETRY_DISABLED=1 to disable telemetry and skip Git discovery.

  • Fixed a tool running twice when the same suspended tool call is approved, declined, or resumed concurrently (for example a double click, a client retry, or two open tabs). approveToolCall, declineToolCall, resumeStream, and their generate variants now claim the suspended run before resuming it. Only one caller resumes; the others fail with AGENT_RESUME_ALREADY_CLAIMED. (#26688)

    const results = await Promise.allSettled([
      agent.approveToolCall({ runId, toolCallId }),
      agent.approveToolCall({ runId, toolCallId }),
    ]);
    // One fulfilled, one rejected with AGENT_RESUME_ALREADY_CLAIMED. The tool runs once.

    Durable agents report the losing resume the same way, as an error chunk in its stream with the AGENT_RESUME_ALREADY_CLAIMED id.

    What each resume error means:

    • AGENT_RESUME_ALREADY_CLAIMED: another approval or resume of this run is running or just finished. Stop retrying.
    • AGENT_RESUME_RUN_NOT_SUSPENDED (durable agents): the run exists but isn't waiting for approval, for example because it already finished or failed. details.status gives its state.
    • AGENT_RESUME_NO_SNAPSHOT_FOUND: there is no record of a suspended run. Check the runId. Agents delete a run's record when it finishes, so an approval sent after that also gets this error.

    Resuming a workflow run that isn't suspended now throws an error with the id WORKFLOW_RUN_NOT_SUSPENDED. The message is unchanged.

    Storage adapters that support atomic updates (for example PostgreSQL, LibSQL, MongoDB, and the in-memory store) protect against duplicates across processes. Other adapters protect against duplicates within a single process.

  • Fixed durable, evented, and Inngest agents leaking one run's messages into another run's model prompt when two Mastra instances in the same process reuse a runId. (#26787)

    Also in: @mastra/inngest@1.10.5

  • Fixed durable agents returning empty step data in callbacks and results. onStepFinish, onFinish, onAbort, output.steps and output.response now include each step's response messages and model id, the same as a regular agent. A model passed to a durable agent's stream() call is now the model that actually runs, instead of the agent's default model. (#26780) (#26933)

  • Document that in-memory, LibSQL, and PostgreSQL workflow result updates no-op after run deletion while remaining adapters are aligned under COR-1448. (#26901)

  • Fixed agent experiment results losing their error details. A failed agent run now stores error.message and error.stack instead of an empty object. Added exported AgentExperimentOutput, ScorerExperimentOutput, and ExperimentResultOutput types that describe what each experiment target stores in output. (#26726)

    Also in: @mastra/client-js@1.54.0, @mastra/react@1.10.0, @mastra/server@1.77.0

  • Fixed output guardrails leaving blocked replies in memory. When an output processor aborts in processOutputResult, the agent (including durable agents) now deletes the reply messages that the run had already saved. The blocked text no longer stays in memory and is not sent back to the model on the next turn. This affected turns that paused for tool approval (approved or declined) and turns run with savePerStep: true. The user's message, earlier turns, and messages sent in as input are kept. Fixes #26215. (#26252)

  • Added targetType to experiment results. The result output is now typed based on the target, so checking targetType gives you typed agent and scorer outputs: (#26726)

    const { results } = await client.listDatasetExperimentResults(datasetId, experimentId);
    
    for (const result of results) {
      if (result.targetType === 'agent') {
        console.log(result.output?.text, result.output?.error?.message);
      } else if (result.targetType === 'scorer') {
        console.log(result.output?.score);
      }
    }

    Workflow, processor, and caller-driven results (targetType: null) keep output as unknown.

    Also in: @mastra/client-js@1.54.0, @mastra/react@1.10.0, @mastra/server@1.77.0

  • Workflow steps, conditions, and dynamic sleeps now create their writer stream only when your code reads writer. Steps that never use it no longer allocate a stream on every run, which reduces memory churn in agent loops. An agent turn, including a durable agent turn, now creates only the writers its tools use. Steps that call writer.write() or writer.custom() behave as before. (#26665)

  • Fixed mastra.datasets.compareExperiments() silently comparing only the baseline and one other experiment. Every ID in experimentIds now gets a result entry on each item (null when that experiment did not run the item). Duplicate IDs are ignored, and passing a baselineId that isn't one of experimentIds now throws instead of being ignored. (#26725)

    const comparison = await mastra.datasets.compareExperiments({
      experimentIds: ['exp-baseline', 'exp-prompt-v2', 'exp-new-model'],
      baselineId: 'exp-baseline',
    });
    // comparison.items[0].results has entries for all three experiments
  • Fixed evented agents and nested evented workflows failing with Workflow with id ... not found when an orchestration worker runs steps on the API server through MASTRA_STEP_EXECUTION_URL. The worker now sends the parent workflow with each step request, so the server can find steps that are only reachable through their parent workflow, such as the steps of an evented agent. Fixes #26676. (#26695)

    Also fixed nested evented workflows hanging when their parent workflow is registered under a key that differs from its id.

    Also in: @mastra/server@1.77.0

  • Fixed delayed interruption of Evented agent responses while preserving partial output and graceful run cleanup. (#26926)

  • Fixed core builds failing when regenerated model provider types grow large. (#26876)

  • Fixed tools returned by an input processor's processInputStep (such as ToolSearchProcessor's loaded tools) running with an empty context.agent.agentId. They now receive the agent's id, the same as tools assigned to the agent directly, in both regular and durable agents. (#26816)

  • Fixed TypeScript types for Standard Schemas that transform their input, such as Effect's Schema.toStandardSchemaV1(Schema.DateFromString). Tool execute functions now receive the transformed output type instead of a union of input and output types. Tool requestContextSchema now accepts these schemas, and tool request context is typed with the transformed output. (#26815)

    const DateSchema = Schema.toStandardSchemaV1(Schema.Struct({ date: Schema.DateFromString }));
    
    const tool = createTool({
      id: 'schedule',
      description: 'Schedules an event',
      inputSchema: DateSchema,
      requestContextSchema: DateSchema,
      execute: async ({ date }, { requestContext }) => {
        date.toISOString(); // date is typed as Date
        requestContext.get('date').toISOString(); // request context value is typed as Date
      },
    });

    Also in: @mastra/schema-compat@1.3.14

  • Fixed the StorageMCPClientToolsConfig and StoredMCPClientToolsConfig JSDoc. It previously said that omitting tools includes all MCP tools. For an agent-level MCP client reference, omitting tools selects no tools. Use tools: {} to include all tools. (#26738)

    Also in: @mastra/client-js@1.54.0

  • ExpectedStep now supports provider_tool_call steps, so you can write typed trajectory expectations for provider-executed tools such as web search. The trajectory scorer compares their toolArgs, toolResult, and success fields. (#26778)

    const steps: ExpectedStep[] = [
      { name: 'web_search', stepType: 'provider_tool_call', toolArgs: { query: 'weather' } },
    ];

    Also in: @mastra/evals@1.10.7

  • Fixed provider tool errors being sent back to the model as successful results. When a provider-run tool returns an error, Mastra now saves it as an error and sends it back as an error in later steps and recalled conversations. (#26652)

  • Fixed live scorers missing tool calls when observational memory is enabled. Observational memory removes messages from the agent's context once it has observed them, and live scorers only saw the messages that were left. On runs that suspend and resume, scorer output could miss every tool call made before suspend(), or end up empty. Scorers now receive every response message the run produced, across all suspend/resume segments, under the original runId. The prompt sent to the model is unchanged. (#26687)

    Also in: @mastra/inngest@1.10.5, @mastra/memory@1.37.1

  • Fixed reasoning from later steps being dropped when the reasoning text is empty. This affected providers that store reasoning identity outside the text, such as Anthropic, Bedrock and Gemini signatures or redacted thinking. Each distinct reasoning block now stays in the conversation when its provider metadata, provider options or reasoning details have an item ID, signature, redacted data, encrypted content or digest field. Identical reasoning blocks are not added again. (#26941)

  • Fixed observational memory's temporal-gap reminder appearing after the user message it describes. A signal can set metadata.precedesMessageId to a message already in the conversation. That signal now sorts directly before the message in the prompt. Saved history keeps the same order. Models no longer read the reminder as the latest turn. (#26800)

  • Updated the observation.blockAfter documentation for Observational Memory: between messageTokens and blockAfter only background buffering and activation run, and a blocking observation runs at or above blockAfter when no buffered chunk activates. (#22078)

  • Fixed structuredOutput so it only uses the final answer of a run. (#26639)

    • Text an agent writes before calling a tool (like "Let me look that up.") no longer fails validation or ends the run early.
    • stream.object now resolves to the final answer, not an earlier JSON draft.
    • Durable agents now return the final answer instead of the first JSON they saw.

    (#25038)

  • Fixed untilIdle streams dropping background tasks started by the run they replaced: a new stream({ untilIdle: true }) on the same thread aborts the earlier idle wrapper, and the new wrapper only tracked tasks whose events it saw itself — so an in-flight task's completion woke nobody and the agent never reported its result. The idle loop now adopts still-running and pending background tasks for its agent, thread, and resource when it starts, so their completions trigger a continuation turn as usual. Fixes #26841 (#26848)

  • Preserve output processor retry metadata in durable step events (#26898)

  • Improved AgentController sessions so each thread restores its saved mode, model, state, token usage, and pending tool prompts when selected. Switching away and detachFromCurrent() cancel actively running local executions only for the initiating session. Listeners detach without cancelling the run. Parked runs and pending prompts remain available when switching back. (#26058)

    Fixed first-thread creation to retain explicit startup mode, model, and thinking selections. Later new conversations use controller defaults. Asynchronous state writes and resumed tools keep their source thread's preferences without changing another active conversation. Addressed resumes recover the originating agent after restart and support repeated suspensions while another thread stays active. The executing run saves token usage to its own thread and retains unsaved measurements for retry on the next step. Subscribers display saved totals without counting replayed steps again or saving usage. Storage-free sessions can still receive inactive wake signals. Custom state and permissions remain host-scoped.

    Changed session.thread.set() and session.thread.clear() to throw during thread lifecycle transitions. Added address-based suspension APIs and event fields so callers can safely claim and resume a prompt after switching threads: const claim = session.claimToolSuspension(toolCallId, runId) returns an address that can be passed to session.respondToToolSuspension({ address, resumeData }). Exported SuspensionAddress and getSuspensionAddressKey for hosts that persist or key those addresses.

    Deprecated the SessionRecord.modeId, modelId, state, and pending fields, the HarnessStorage.appendPendingItem(), updatePendingItem(), and removePendingItem() helpers, and SessionSuspensions.resolveToolCallId().

@mastra/ai-sdk@1.11.1

Patch Changes

  • Fixed v6 chat requests with several tool approvals stopping early. If another request is already resuming one approval, the remaining approvals now still run. (#26688)

@mastra/auth-better-auth@1.1.7

Patch Changes

  • security: bump better-auth to ^1.7.7 and @modelcontextprotocol/sdk to ^1.32.1 for Vanta/Aikido findings (AIKIDO-2026-373183, AIKIDO-2026-261025, AIKIDO-2026-365378/156139/344432). (#26773)

    Also in: @mastra/mcp-registry-registry@1.1.5, @mastra/playground-ui@63.0.0

@mastra/braintrust@1.3.21

Patch Changes

  • Fixed the Braintrust exporter's flush() and shutdown() so they wait for Braintrust's background logger to send queued spans, scores, and abort updates instead of returning early. (#26759)

@mastra/clickhouse@1.25.0

Minor Changes

  • Added clickhouse_settings to ObservabilityStorageClickhouseVNext and the other ClickHouse storage domains, so settings such as filesystem_prefetches_limit can be applied to every query without passing a pre-configured client. (#26327)

    const observabilityStore = new ObservabilityStorageClickhouseVNext({
      url: process.env.CLICKHOUSE_URL!,
      username: process.env.CLICKHOUSE_USERNAME!,
      password: process.env.CLICKHOUSE_PASSWORD!,
      clickhouse_settings: { filesystem_prefetches_limit: 8 },
    });

Patch Changes

  • Fixed trace queries and trace aggregates dropping a trace when another tenant stored a trace with the same trace ID. Scoped trace queries and aggregates now read only that tenant's trace roots when picking each trace's current root. (#26327)

@mastra/client-js@1.54.0

Minor Changes

  • client.compareExperiments() now compares two to ten experiments at once against a baseline. Pass experimentIds and optionally baselineId: (#26725)

    await client.compareExperiments({ datasetId, experimentIds: ['exp-1', 'exp-2', 'exp-3'], baselineId: 'exp-1' });

    experimentIdA / experimentIdB still work for comparing two experiments, but are deprecated in favor of experimentIds.

Patch Changes

  • Added the optional run id field to generated Agent Controller tool-suspension route types. (#26058)

@mastra/cloudflare@1.7.3

Patch Changes

  • Added run fencing for durable agents. Durable Objects stores (CloudflareDOStorage) now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26423)

    Claims are kept in two new tables, mastra_workflow_run_owners and mastra_memory_run_fences, which are created on init. If you set disableInit: true, create them before upgrading. Durable agent runs fail to start until the tables exist. CloudflareKVStorage doesn't support run fencing.

    storage.prune() doesn't cover these tables on Durable Objects yet, so their rows are kept.

    Requires @mastra/core 1.76.0 or a later 1.x release.

@mastra/cloudflare-d1@1.4.2

Patch Changes

  • Added run fencing for durable agents. D1 stores now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26423)

    Claims are kept in two new tables, mastra_workflow_run_owners and mastra_memory_run_fences, which are created on init. If you set disableInit: true, create them before upgrading. Durable agent runs fail to start until the tables exist.

    storage.prune() doesn't cover these tables on D1 yet, so their rows are kept.

    Requires @mastra/core 1.76.0 or a later 1.x release.

@mastra/code-sdk@1.13.0

Minor Changes

  • The theme preference now accepts any theme name, not just auto, dark, or light, so callers can persist bundled and custom theme selections. (#26719)

  • Added the instructionRoots session state field. A host that checks out several repositories under one working directory lists their directories, and the system prompt loads each one's AGENTS.md or CLAUDE.md in that order. Without it, instructions load from the working directory as before. (#26169)

    await controller.setState({
      projectPath: '/workspace',
      instructionRoots: ['api', 'web'], // loads /workspace/api/AGENTS.md, then /workspace/web/AGENTS.md
    });

    On an untrusted checkout with a baseRef, only the first root loads, from that ref.

Patch Changes

  • Fixed cancellation of pending tool approvals to target the originating run and ignore prompts that have already been resolved. (#26058)

  • The default memory from createMastraCode() now has a settled() method. Await it before closing storage so background memory work, such as buffered observations and their indexing, finishes first. (#22078)

    const { memory, storageMaintenance } = await createMastraCode();
    if (memory && 'settled' in memory) await memory.settled();
    await storageMaintenance?.closeStorage?.();
  • New Mastra Code threads retain a stable local owner identity across sessions. (#26549)

@mastra/connect@1.0.2

Patch Changes

  • Jira tool errors now include the human-readable message from errorMessages[] and field-level errors maps, and GitHub-style { message } error bodies surface their message instead of a bare status code. (#26762)

  • Fixed Linear tool calls failing on valid API responses: attachment tools now accept null creator emails, and cycle/project tools report GraphQL errors instead of crashing on validation. Fireflies transcript tools now handle action_items returned as either an array or a newline-joined string. (#26516)

  • @mastra/connect now requires @mastra/core 1.76.0 or later, matching the @mastra/mcp version it depends on. (#26059)

@mastra/convex@1.7.3

Patch Changes

  • Added run fencing for durable agents. Convex stores now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26423)

    Claims are stored in the existing mastra_documents table, so no schema change is needed. Redeploy your Convex functions after upgrading. Until you do, durable agent runs fail with an error asking you to redeploy. Other writes keep working.

    storage.prune() doesn't cover these claims on Convex yet, so they are kept.

    Requires @mastra/core 1.76.0 or a later 1.x release.

  • Redeploy your Convex schema and functions when you upgrade. This version calls new observational memory server operations and adds an optional supersededBy field; against functions deployed from an older version, observational memory writes fail until you redeploy. (#25906)

    Fixed observational memory losing buffered observations or activated context when reflections, observation activation, and background buffering overlap. Reflections and initialization now run as single server mutations, and every write checks that it targets the current generation. Older duplicate generations are marked superseded when a write targets them. Protection is complete once every process and the deployed functions run the new version.

@mastra/datadog@1.4.16

Patch Changes

  • Datadog exporter and bridge flush() and shutdown() now wait a bounded time for dd-trace's background writer to send LLM Observability spans and evaluations, so a process that flushes and then calls process.exit() or gets frozen (serverless) no longer cuts the send off right away. The wait is best-effort: dd-trace gives no way to confirm the request completed. Tune it with the new flushGraceMs option (default 2000 ms, 0 disables): (#26761)

    new DatadogExporter({ mlApp: 'my-app', flushGraceMs: 3000 });
  • Span links are now sent to Datadog as span links, from both DatadogExporter and DatadogBridge. (#26059)

@mastra/docker@0.11.0

Minor Changes

  • Added DockerFactorySandbox, which plugs Docker into Mastra Factory as the sandbox provider. Factory gets one container per session, a repository image it can build ahead of the first session, and user-tunable settings. It takes the same options as DockerSandbox except id and template. (#26467)

    import { MastraFactory } from '@mastra/factory';
    import { DockerFactorySandbox } from '@mastra/docker';
    
    new MastraFactory({
      sandbox: new DockerFactorySandbox({ defaults: { baseImage: 'node:22-slim' } }),
    });

    Settings: baseImage, the image the repository image builds from, and owner, the user[:group] that owns the checked-out repositories in it. Unset values fall back to the defaults option, then to node:22-slim and root.

    Builds: builds the repository image in the background and reports its status. Builds live in the host process, so after a restart an earlier build id reads as unknown. No build history.

@mastra/dsql@1.5.2

Patch Changes

  • Added run fencing for durable agents. Aurora DSQL stores now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26421)

    Claims are kept in two new tables, mastra_workflow_run_owners and mastra_memory_run_fences, which are created on init. If you set disableInit: true, create them before upgrading. Durable agent runs fail to start until the tables exist.

    storage.prune() doesn't cover these tables on Aurora DSQL yet, so their rows are kept.

    Requires @mastra/core 1.76.0 or a later 1.x release.

@mastra/dynamodb@1.4.2

Patch Changes

  • Added run fencing for durable agents. DynamoDB stores now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26423)

    Claims are stored as items in your existing table, so no table changes are needed. storage.prune() doesn't cover these items on DynamoDB yet, so they are kept.

    Requires @mastra/core 1.76.0 or a later 1.x release.

@mastra/e2b@0.14.0

Minor Changes

  • Added E2BFactorySandbox, which plugs E2B into Mastra Factory as the sandbox provider. Factory gets one sandbox per session, a repository template it can build ahead of the first session, and user-tunable settings. (#26467)

    import { MastraFactory } from '@mastra/factory';
    import { E2BFactorySandbox } from '@mastra/e2b';
    
    new MastraFactory({
      sandbox: new E2BFactorySandbox(), // reads E2B_API_KEY
    });

    Settings: cpuCount, memoryMb and idleTimeoutMinutes. Unset values fall back to the defaults option, then to 2 CPUs, 1024 MB and 5 minutes.

    Builds: starts a template build in the background, reports its status with logs, and lists the template's build history.

@mastra/factory@0.22.0

Minor Changes

  • Added environment builds. When the Factory sandbox implements the builds capability, Factory builds the environment template ahead of sessions instead of on the first session boot. (#26479)

    • POST /web/factory/projects/:id/environment/build starts a build now (the Environment page's Build now button, or mastra api factory project environment build <project-id>).
    • GET /web/factory/projects/:id/environment/builds and GET /web/factory/projects/:id/environment/builds/:buildId read the provider's build history and live status.
    • The environment payload carries buildTriggers: a cron schedule and a push trigger with a debounce window.
    • Scheduled and push builds skip when the last build is ready and nothing it was built from changed. Only one build starts at a time, and no trigger starts a build while the last one is still running. Saving settings never starts a build.
    • Hosts that spread prepare()'s result into new Mastra pick up the factory-environment-build workflow.
    • After a session's own setup, each repository link records configured or failed with the redacted error.

    Turn on a nightly rebuild and push builds for a project:

    await fetch(`${baseUrl}/web/factory/projects/${projectId}/environment`, {
      method: 'PATCH',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        buildTriggers: {
          schedule: { enabled: true, cron: '0 3 * * *', timezone: 'UTC' },
          push: { enabled: true, debounceMinutes: 10 },
        },
      }),
    });
  • Sessions boot every repository in the Factory environment instead of one linked repository. (#26169)

    • The sandbox template is built from every environment repository, in the order shown on the Environment page. Each is cloned under the workspace root, which becomes the session's working directory.
    • The session branch is resumed where the remote has it. A new session branch starts from the latest remote default branch, and local work in an existing checkout is kept.
    • A repository's setup command is skipped when the template already ran it, and the workspace setup command runs last.
    • A factory-environment state signal tells the agent which repositories are present, where, and on which branch.
    • Every repository is torn down when the session retires.
    • The agent loads each environment repository's AGENTS.md or CLAUDE.md and project skills, starting with the session's own repository. Review sessions load only the session's own repository's instructions, from its base branch.
    • The provider receives the project's stored sandbox settings when a session's sandbox is constructed. Settings saved later apply to sandboxes constructed after that.
    class MyFactorySandbox extends FactorySandbox<{ cpuCount?: number }> {
      readonly provider = 'my-provider';
      readonly settings = z.object({ cpuCount: z.number().int().min(1).optional() });
    
      create(ctx: FactorySandboxContext, settings: { cpuCount?: number }) {
        // ctx.repos lists every environment repository; ctx.workingDirectory is the workspace root.
        return new MySandbox({ id: ctx.sessionId, cpus: settings.cpuCount ?? 2, workingDirectory: ctx.workingDirectory });
      }
    }
  • Added FactorySandbox support to MastraFactoryConfig.sandbox. Pass a provider instance (PlatformFactorySandbox, E2BFactorySandbox, DockerFactorySandbox, or the new LocalFactorySandbox exported from @mastra/factory) and the factory gains that provider's environment settings and template builds. The callback form still works and is wrapped as a provider: 'custom' sandbox with no settings. (#26467)

    import { MastraFactory, LocalFactorySandbox } from '@mastra/factory';
    import { E2BFactorySandbox } from '@mastra/e2b';
    
    // Before: a callback building one sandbox per session
    new MastraFactory({ sandbox: ctx => new E2BSandbox({ id: ctx.sessionId, sandboxId: ctx.sandboxId }) });
    
    // After: a provider instance, which also carries settings and builds
    new MastraFactory({ sandbox: new E2BFactorySandbox() });
    
    // Or run sessions on the host machine, one directory per session
    new MastraFactory({ sandbox: new LocalFactorySandbox({ root: '/var/sandboxes' }) });
  • Added a sandbox environment to each Factory project. Provider settings, the workspace setup command, the working directory and the order repositories are checked out in now live on the project and are read and written through one route, GET and PATCH /web/factory/projects/:id/environment. (#26115)

    {
      "settings": { "cpuCount": 8 },
      "workspaceSetupCommand": "pnpm -r build",
      "sandboxWorkingDirectory": "/home/user",
      "repositories": [
        { "projectRepositoryId": "…web", "position": 1 },
        { "projectRepositoryId": "…docs", "position": 2, "inEnvironment": false }
      ]
    }

    Settings: only the keys you set are stored; anything unset keeps the provider default. A patch is merged onto the stored settings, null removes a key, and the result is checked against the provider's schema before anything is written. A rejected value answers 400 invalid_environment naming the field; a factory without a sandbox answers 400 no_sandbox.

    Repositories: to reorder, list every repository with positions 1 through n. A second link to the same repository starts outside the environment.

    Existing projects keep working: their links get an order on the next start and nothing is deleted.

  • Sessions in a multi-repository environment can push and open change requests in any environment repository. (#26334)

    • Every source_control_* tool except source_control_refresh_change_request_checkout (the GitLab checkout refresh) takes an optional repository slug; the default is the repository the session is filed under, and each tool description lists the valid slugs.
    • The first push to another repository sends the commits checked out there as the session branch, and the agent's later commits in that repository go to the same branch and are sent by its next push.
    • Pushing a repository that has no checkout in the session fails with a clear error.
    • Every push and change request is recorded per repository, and a push after a change request keeps the change request fields.
    • The gh pr create observer and auto-subscribe attribute a pull request to its repository and accept any environment repository. Only a pull request from the session branch is recorded on the session; subscribing to someone else's leaves the record alone.
    • github_subscribe_pr / github_unsubscribe_pr and pull request subscriptions are gated on the Factory, so web user sessions get them too.
    • On the platform GitHub provider, GH_TOKEN covers every environment repository when they share one installation and number at most ten; otherwise it covers the session repository and the agent is told to use the source_control_* tools for the others.

    For example, an agent in a session filed under acme/api pushes and opens a change request in acme/docs:

    { "tool": "source_control_push_branch", "input": { "repository": "acme/docs" } }
    { "tool": "source_control_create_change_request", "input": { "repository": "acme/docs", "title": "Document the new endpoint" } }
  • Sessions belong to a Factory. A session now records the Factory it belongs to, not only the repository link it was created on. This is groundwork for sessions that span every repository in the Factory environment. (#26123)

    • List every session of a Factory the caller can see, across all source-control providers, with GET /web/factory/projects/:id/sessions. The sidebar, search and board use it instead of one request per repository.
    • sessions.create on the source-control handle now requires the Factory and rejects one that does not match the repository link.
    // Before
    await sourceControl.sessions.create({ sessionId, projectRepositoryId, orgId, userId, branch, baseBranch });
    
    // After
    await sourceControl.sessions.create({
      sessionId,
      projectRepositoryId,
      factoryProjectId,
      orgId,
      userId,
      branch,
      baseBranch,
    });

    Upgrading: existing sessions are assigned their Factory on the next start. A session whose repository link no longer exists, or that holds the same branch for the same user as an older session on another repository of the Factory, stays unassigned. Unassigned sessions don't appear in the Factory session list, and Slack replies in their threads skip Factory model defaults. Continue that work in a new Slack thread or a new session. A board run that would reuse one fails with an error naming the session to delete before retrying.

Patch Changes

  • Fixed environment updates that reorder repositories leaving two repositories at the same position. The project fields, settings and every repository change in one PATCH /web/factory/projects/:id/environment now save together or not at all, and concurrent reorders apply one after the other. (#26115)

  • Fixed concurrent environment settings patches losing updates by merging and validating against the current project inside a database transaction. (#26115)

  • Fixed the Factory web UI hanging on the splash screen when authentication is disabled (auth: null or MASTRACODE_AUTH_DISABLED=1). GET /auth/me now returns 404, so the UI shows that no authentication provider is configured instead of retrying forever. (#26797)

  • Fixed a reused Factory workspace keeping the reviewer GitHub token when the run-binding lookup fails. A session whose role cannot be read now falls back to the worker token (or repository access), and refresh callbacks from the earlier reviewer context are rejected. The reviewer token comes back once the review binding is readable again. (#26804)

  • Redesigned Factory onboarding around an editable draft and a final review. Repository and model choices stay editable and survive redirects and reloads in the onboarding draft. They are applied to the Factory only when you select Create factory. If confirmation fails, retrying reuses the same pending Factory instead of creating a duplicate and only replaces repository links onboarding created. Create factory stops with a clear message when the chosen model is no longer available. (#26509)

    Model providers

    • API keys and provider sign-in are now separate choices, each with explicit organization or personal scope.
    • Provider and model lists offer Retry when they fail to load.
    • Members without shared organization access choose a personal model, which becomes both the Factory default and their own default.

    Codebase and work

    • Selecting a repository no longer moves you on by itself; Continue does.
    • Repository lists and GitLab status offer Retry when they fail to load, instead of reporting GitLab as unavailable.
    • Jira, incident.io, and Linear offer Reconnect when a connection needs to be authorized again.
    • Create factory starts a fresh Factory when the pending one is no longer accessible.

    A contextual illustration follows each step with smooth transitions and continuous work previews. The artwork and animation are implemented in the app, pause when offscreen, and respect reduced-motion settings.

  • Fixed enabling secretEncryption on an existing Factory database. Model credentials, custom provider API keys, and integration tokens saved before encryption was turned on now read back correctly and are encrypted in place. Previously, they were encrypted as JSON text, so stored credentials stopped working. (#26890)

  • Factory skills settings now flag a skill the agent cannot load. Skills are checked with the same frontmatter parser the agent uses, so a SKILL.md with invalid YAML (for example an unquoted : in the description) shows "Failed to load" with the parse error, instead of looking healthy and then failing at kickoff with "skill not found". (#26788)

  • Added a persisted modelScope field to Factory projects, defaulting to factory to preserve the existing Factory-level behavior. This release saves and exposes the selection for future support; it does not change session model or credential resolution. (#26803)

    Consumers can inspect the saved setting through the exported FactoryProject type:

    import type { FactoryProject } from '@mastra/factory';
    
    function usesFactoryDefaults(project: FactoryProject) {
      return project.modelScope === 'factory';
    }
  • Fixed environment migration to leave the workspace root unset instead of copying a legacy repository checkout path. Preserve explicitly configured roots and cleared values across initialization. (#26115)

  • Removed the commit id from the review verdict badge on Reviewing cards. The badge now reads "Approved" or "Changes requested", and still adds "(outdated)" once new commits are pushed after the review. (#26770)

  • Sessions with several repositories now report changes and touched files per repository. (#26542)

    • Each repository's changes carry its slug and provider.
    • Touched files are grouped by checkout directory, and files written at the workspace root during the session are listed too. Dotfiles such as .env are never listed.
    • The agent's file tools and project path start at the workspace root, and each repository's AGENTS.md and project skills load.

    Sessions with one repository work as before.

  • Improved GitHub API latency in Factory. GitHub calls for an installation now reuse its access token until it nears expiry, instead of requesting a new token before every call. After the first call, each GitHub call takes about half as long, and Factory requests far fewer tokens, reducing the risk of hitting GitHub's token rate limits. (#26709)

@mastra/fastembed@1.3.3

Patch Changes

  • Fixed embeddings failing with "Config file not found" when the model cache folder is removed, or the home directory changes, while a process is running. The cache folder is now looked up each time a model loads instead of once per process. (#22078)

@mastra/langfuse@1.6.0

Minor Changes

  • Added top-level observation metadata in Langfuse for scalar span attributes (string, number, or boolean), for example stepIndex and isContinued on model step spans. Observation-level LLM-as-a-Judge rules can now filter on them. No code change is needed: (#26727)

    import { Observability } from '@mastra/observability';
    import { LangfuseExporter } from '@mastra/langfuse';
    
    const observability = new Observability({
      configs: {
        langfuse: { exporters: [new LangfuseExporter()] },
      },
    });

    In Langfuse, filter an observation-level evaluator on spanType equals model_step and isContinued equals "false" to judge only the final step of each agent run. Attributes that hold objects or arrays, such as workflow entryMetadata, aren't promoted.

@mastra/langsmith@1.3.26

Patch Changes

  • Fixed the LangSmith exporter sending more than one update per run. Span updates now only change the local run, and a single update with the final outputs, metadata and end time is sent when the span ends. Before, LangSmith dropped or rejected the final update, so traces kept partial outputs and a wrong end time. (#26767)

  • Fixed LangSmith exporter losing runs: flush() now sends queued batches when the client uses manualFlushMode (overlapping calls wait for the earlier upload), and shutdown() sends the updates for spans it aborts instead of leaving them pending. (#26760)

@mastra/libsql@1.26.1

Patch Changes

  • Fixed LibSQLFactoryStorage.withTransaction on :memory: databases. It ran the callback without a transaction, so a write that failed partway left the earlier writes in place. It now commits or rolls back like a file database. (#26115)

  • Prevent late workflow result writes from recreating deleted runs. (#26901)

    Also in: @mastra/pg@1.31.1

  • Fixed observational memory losing context when several operations run at the same time. (#25906)

    • Buffered observations and activated context are no longer lost when reflection, activation, and background buffering overlap, even across processes that share one database file.
    • Fixed SQLITE_BUSY: database is locked errors when observational memory and message saves run at the same time on a local file database.
    • With Turso embedded replicas (url plus syncUrl), an observational memory write syncs the replica before it retries after a conflict, or before it treats a record another instance created as missing. This does not apply to a prebuilt client.

    Upgrading. A nullable supersededBy column is added to the observational memory table automatically at startup. Existing rows are backfilled at the next startup of any process, so older generations stop accepting writes. Protection is complete once every process that shares the database runs the new version; older versions running alongside it neither set nor check the new column.

    If you manage the schema yourself (disableInit: true or MASTRA_DISABLE_STORAGE_INIT=true), startup does not run this migration. Add a nullable supersededBy column, of the same type as id, to mastra_observational_memory before you upgrade, or reflections and buffered activation fail with a missing-column error.

@mastra/loggers@1.3.7

Patch Changes

  • Fixed HttpTransport and UpstashTransport modifying log objects passed to them. When a record has no timestamp, the transport now adds time to its own copy, so the caller's object stays unchanged and frozen records are sent instead of failing with a TypeError. (#26892)

  • Fixed UpstashTransport returning empty {} log records for list entries that are not valid JSON. Unparseable entries are now skipped, so log queries return only real records and snapshot totals no longer count corrupt entries. (#26891)

@mastra/mcp@2.4.0

Minor Changes

  • Linked MCP calls across traces, in both directions. When an agent or workflow calls a tool through MCPClient, the request carries the W3C traceparent of the tool call span. MCPServer keeps its own trace for the request and links its request span to that span. Its tools/call reply names the span that served the call, and MCPClient links the tool call span back to it, so you can move between the two traces from either side. When the server uses a tracing bridge and its HTTP instrumentation continues the caller's trace, the request joins that trace instead. (#26059)

    The server reads traceparent and tracestate from the request _meta, or from the HTTP headers that OpenTelemetry-instrumented callers send. It also records them on the request span as callerTraceparent and callerTracestate, so exporters without span link support still show the caller.

    This needs no setup. To send your own trace context, or none, set traceContext on the server:

    const mcp = new MCPClient({
      servers: {
        orders: {
          url: new URL('https://orders.example.com/mcp'),
          traceContext: () => undefined, // send no trace context
        },
      },
    });

    @mastra/mcp now requires @mastra/core 1.76.0 or later.

@mastra/memory@1.37.1

Patch Changes

  • Fixed observational memory erasing schema-based working memory. This happened when the Observer model returned an empty working-memory object, which Gemini does with manageWorkingMemory: true. (#26681)

    An empty working-memory result no longer replaces stored working memory. Mastra now retries the extraction when this happens. Other extracted values, such as the thread title, are kept whether the retry succeeds or fails.

  • Fixed "Expression produces a union type that is too complex to represent" type errors in observational memory when the model list grows. Observer, reflector and fallback model lists no longer make type checking scale with the number of model IDs. (#26908)

  • Observational Memory no longer blocks an agent step on the Observer as soon as unobserved messages reach messageTokens with async buffering enabled. Between messageTokens and observation.blockAfter (default 1.2, so 36k tokens with the default 30k threshold), background buffering keeps running and the next ready chunk activates on a later step. A blocking synchronous observation now runs only when pending tokens still reach blockAfter after buffered activation, and observes only the uncovered tail. Previously, reaching the threshold without a ready chunk forced a blocking Observer call, and buffering couldn't start above the threshold, so long agent sessions blocked on observation turn after turn. (#22078)

    Unobserved messages can now grow up to blockAfter before a step blocks. To keep the previous behavior of observing synchronously at the threshold, set blockAfter: 1:

    observationalMemory: {
      observation: { messageTokens: 30_000, blockAfter: 1 },
    }

    A multiplier blockAfter now scales a per-thread messageTokens override instead of the instance threshold. With blockAfter: 1.2, this thread blocks at 12,000 tokens:

    await memory.updateObservationalMemoryConfig({
      threadId,
      config: { observation: { messageTokens: 10_000 } },
    });

    getStatus() also returns observationBlockAfter and inAsyncObservationBand.

    At blockAfter, activation waits a bounded time for a buffered observation that is still being produced so it activates with the rest. If that takes longer, the step observes the remaining messages synchronously; a late observation whose messages are all already observed is then dropped instead of stored. Activations triggered by the idle TTL or a model change don't wait for buffered observations still in progress.

    Fixed semantic recall missing a turn's new user message when background buffering picked it up on the first step.

  • Fixed out-of-order observations (#26129)

    Observational memory now keeps observations in the order events happened. Before, an important user message could be listed ahead of earlier events from the same period, so an outdated state looked like the latest one. For example, a "plan not approved, wait for revision instructions" result could be recorded after the user's actual revision instructions, and the agent kept waiting for instructions it already had.

  • Fixed observation.previousObserverTokens being ignored. Setting a budget now limits Observer calls to the most recent previous observations that fit it, on every observation path, and the Observer is told when its context was truncated. previousObserverTokens: 0 omits previous observations. The default is now false, which sends the full previous observations. That matches how Observer calls have behaved since the budget stopped being applied, so nothing changes unless you set a budget. Stored memory is unaffected: new observations are still appended to the full observation text. (#26193)

  • Fixed observational memory buffering end markers going missing from saved message history, and a rare case where an agent's final response content could be overwritten. (#26149)

    When background buffering finished, its end marker was written onto the newest assistant message, usually one the agent was still streaming. The agent's next save of that message dropped the marker. If the agent's last save landed while the marker was being written, the marker write could replace the agent's newer content. The end marker now goes on the message that carries the cycle's start marker, and the message the agent is streaming is never rewritten. The start marker now goes on the newest assistant message the cycle buffers, never on the message the agent continues streaming into, so the end marker can always find it.

  • Observational memory now runs its buffering, activation, observation, reflection, and config-override writes for a thread (or resource) one at a time within a process, and a due reflection goes ahead of other waiting writes. Agents and Memory instances that share a thread in one process no longer race each other's buffering, activation, and reflection commits, and a reflection is not held back behind a run of buffered-observation writes. Observer and Reflector model calls still run concurrently. If a storage write has not returned after 60 seconds, later writes for that thread stop waiting for it and a warning is logged, so one stuck database call cannot block the thread's memory. (#25998)

  • Reflections now keep more of the details users ask about later. The default Reflector instructions tell it to keep every concrete fact (names, places, dates, numbers, amounts, versions, and who said what). They also tell it to keep both the old and new value, with dates, when the user changes their mind, and to keep what the user explicitly said they have never done, do not know, or refused. Reflections favor short factual lines over general summaries, and older observations are condensed in wording rather than stripped of their facts. Custom reflection.instruction text is still appended as before. (#26465)

  • Fixed observational memory losing context for the agent when background buffering, observation activation, and reflection overlap, including across processes and durable agent steps. (#25906)

    • Observations buffered or activated while a reflection runs are kept in the new generation instead of being dropped and re-observed later in larger batches.
    • Activation and sync observation commit only to the current generation. Messages leave the agent's context only after their observations are saved there; otherwise the observation retries against the current generation or the messages stay in context.
    • A reflection that was superseded by another one no longer marks itself as completed.
    • Messages saved by another agent or process while an observation runs are no longer marked as observed, and an observation that finds nothing new no longer skips messages saved with earlier timestamps.
    • When two agents or processes observe the same messages at once, the second no longer saves a second copy of those observations.
    • Overlapping background buffering calls for the same thread now run one after another, so the same messages are no longer sent to the Observer and stored more than once.
    • An observation or buffered observation that finishes after its thread's observational memory was cleared (or the thread deleted) is discarded instead of being saved into the thread's new record.
    • Copying a thread while a reflection replaces its observational memory no longer gives the copy a record that rejects writes.

@mastra/mongodb@1.23.1

Patch Changes

  • Fixed observational memory losing buffered observations or activated context when reflections, observation activation, and background buffering overlap, including from several processes. Works on standalone servers and replica sets without transactions. Fixed buffered observations whose text starts with $ being stored incorrectly. (#25906)

    Upgrading. A supersededBy field is set on observational memory documents automatically: existing documents are backfilled at the next startup of any process. Protection is complete once every process that shares the database runs the new version. Safe concurrent use relies on the unique id index on the observational memory collection, which is created by default; if you use skipDefaultIndexes, create it yourself (a warning is logged when it is missing).

  • Added run fencing for durable agents. MongoDB stores on replica sets and sharded clusters now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26423)

    Standalone servers don't support the multi-document transactions this needs, so they keep working as before without run fencing, and durable agents fall back to the PubSub lease. While the server can't be reached to tell which kind it is, durable agent runs fail to start instead of picking a fallback, so two instances can't end up claiming the same run in different ways.

    To prune claims, set retention.workflows.runOwnership and retention.memory.runFences and call storage.prune().

    Requires @mastra/core 1.76.0 or a later 1.x release.

  • Fixed storage.prune() on MongoDB deleting a document that was updated while the prune was running. A document that is no longer older than maxAge when its batch is deleted is now kept. (#26423)

@mastra/mssql@1.10.3

Patch Changes

  • Added run fencing for durable agents. SQL Server stores now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26421)

    Claims are kept in two new tables, mastra_workflow_run_owners and mastra_memory_run_fences, which are created on init. If you set disableInit: true, create them before upgrading. Durable agent runs fail to start until the tables exist.

    storage.prune() doesn't cover these tables on SQL Server yet, so their rows are kept.

    Requires @mastra/core 1.76.0 or a later 1.x release.

@mastra/mysql@0.13.1

Patch Changes

  • Fixed observational memory losing buffered observations or activated context when reflections, observation activation, and background buffering overlap, including from several processes or connection pools. Observational memory writes now lock the row they change and retry on deadlocks. (#25906)

    Upgrading. A nullable supersededBy column is added to the observational memory table automatically at startup. Existing rows are backfilled at the next startup of any process, so older generations stop accepting writes. Protection is complete once every process that shares the database runs the new version; older versions running alongside it neither set nor check the new column.

    If you manage the schema yourself (disableInit: true or MASTRA_DISABLE_STORAGE_INIT=true), startup does not run this migration. Add a nullable supersededBy column, of the same type as id, to mastra_observational_memory before you upgrade, or reflections and buffered activation fail with a missing-column error.

  • Added run fencing for durable agents. MySQL stores now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26421)

    Claims are kept in two new tables, mastra_workflow_run_owners and mastra_memory_run_fences, which are created on init. If you set disableInit: true, create them before upgrading. exportSchemas() includes them. Durable agent runs fail to start until the tables exist.

    storage.prune() doesn't cover these tables on MySQL yet, so their rows are kept.

    Requires @mastra/core 1.76.0 or a later 1.x release.

@mastra/next@0.2.35

Patch Changes

  • Route handlers now retry initialization on the next request after a failed init, instead of failing every request until the process restarts. Concurrent requests during initialization still share a single init. (#26784)

    Also in: @mastra/tanstack-start@0.2.35

@mastra/observability@1.20.0

Minor Changes

  • Added cost estimates for models that models.dev doesn't price, such as private deployments, using the pricing a custom gateway returns from fetchProviders(). Prices load on the first call that has no price, and also when Mastra starts under mastra dev or with MASTRA_AUTO_REFRESH_PRICING=true. A gateway that fails to load is logged and retried about once an hour. Set MASTRA_OFFLINE=true to skip loading them. (#26875)

  • Cost estimates now use model prices from models.dev. Each release of @mastra/observability includes a recent snapshot of these prices. (#26518)

    Models that models.dev no longer lists, mostly retired ones such as claude-3-7-sonnet, o1-mini and gemini-1.5-pro, no longer get cost estimates. Their token metrics are still recorded, without cost fields.

    Prices can also refresh from models.dev while your app runs:

    • Under mastra dev, refresh is on by default. Prices refresh in the background once a day, and when a model has no price. Refreshed prices are cached in ~/.cache/mastra. Set MASTRA_AUTO_REFRESH_PRICING=false to turn it off.
    • In other environments, such as production, refresh is off by default. Set MASTRA_AUTO_REFRESH_PRICING=true to turn it on.
    • MASTRA_OFFLINE=true turns refresh off everywhere, whatever MASTRA_AUTO_REFRESH_PRICING is set to.

    The model label on cost metrics now uses the models.dev model ID for some models, mostly gateway, OpenRouter and Amazon Bedrock models. After you upgrade, charts grouped by model can show these models under a new name.

Patch Changes

  • Update bundled model pricing with the latest prices from models.dev (6e5f3b3)

  • Spans now keep and export the links they were started with, and any links set on the span before it ends. Links whose IDs are not valid W3C trace and span IDs (32 and 16 lowercase hex characters, not all zeros) are ignored. (#26059)

  • MastraStorageExporter and DefaultExporter flush() and shutdown() now wait for storage writes that a batch timer or size trigger already started, so spans are no longer lost when a process exits right after flushing. (#26746)

@mastra/oracledb@0.5.2

Patch Changes

  • Added run fencing for durable agents. Oracle stores now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26421)

    Claims are kept in two new tables, mastra_workflow_run_owners and mastra_memory_run_fences, which are created on init. If you set disableInit: true, create them before upgrading. exportSchemas() includes them. Durable agent runs fail to start until the tables exist.

    storage.prune() doesn't cover these tables on Oracle yet, so their rows are kept.

    Requires @mastra/core 1.76.0 or a later 1.x release.

  • Fixed observational memory losing buffered observations or activated context when reflections, observation activation, and background buffering overlap, including from several processes or connection pools. Fixed observational memory timestamps being stored up to an hour off when the server process runs in a time zone with daylight saving time; rows written before this fix keep their stored values. (#25906)

    Upgrading. A nullable supersededBy column is added to the observational memory table automatically at startup. Existing rows are backfilled at the next startup of any process, so older generations stop accepting writes. Protection is complete once every process that shares the database runs the new version; older versions running alongside it neither set nor check the new column.

    If you manage the schema yourself (disableInit: true or MASTRA_DISABLE_STORAGE_INIT=true), startup does not run this migration. Add a nullable supersededBy column, of the same type as id, to mastra_observational_memory before you upgrade, or reflections and buffered activation fail with a missing-column error.

@mastra/otel-bridge@1.5.16

Patch Changes

  • Span links are now sent as OpenTelemetry span links, so backends such as Jaeger and Grafana Tempo show which span in another trace a span is related to. (#26059)

    Also in: @mastra/otel-exporter@1.4.7

  • OpenTelemetry bridge flush() and shutdown() now flush the global tracer and logger providers registered by NodeSDK, and end leftover spans before flushing. (#26758)

@mastra/otel-exporter@1.4.7

Patch Changes

  • Fixed OtelExporter.flush() so it waits for spans that are still being sent before it returns. Serverless code that flushes and then exits no longer loses those spans. Arize and Arthur exporters get the same fix. (#26757)

@mastra/pg@1.31.1

Patch Changes

  • Fixed observational memory losing buffered observations or activated context when reflections, observation activation, and background buffering overlap, including from several processes or connection pools. Observational memory writes now lock the row they change, and the current generation is always read from the primary, never a lagging read replica. Fixed lastBufferedAtTime being read back shifted by the host's time-zone offset. (#25906)

    Upgrading. A nullable supersededBy column is added to the observational memory table automatically at startup. Existing rows are backfilled at the next startup of any process, so older generations stop accepting writes. Protection is complete once every process that shares the database runs the new version; older versions running alongside it neither set nor check the new column.

    If you manage the schema yourself (disableInit: true or MASTRA_DISABLE_STORAGE_INIT=true), startup does not run this migration. Add a nullable supersededBy column, of the same type as id, to mastra_observational_memory before you upgrade, or reflections and buffered activation fail with a missing-column error.

@mastra/platform-workspace@1.8.0

Minor Changes

  • Added PlatformFactorySandbox, which plugs Mastra Platform sandboxes into Mastra Factory as the sandbox provider. Factory gets one sandbox per session, a repository template it can build ahead of the first session, and user-tunable settings. It takes the same options as PlatformSandbox except the per-session ones (id, sandboxId, sessionId, template). (#26467)

    import { MastraFactory } from '@mastra/factory';
    import { PlatformFactorySandbox } from '@mastra/platform-workspace';
    
    new MastraFactory({
      sandbox: new PlatformFactorySandbox(), // reads MASTRA_PLATFORM_ACCESS_TOKEN and MASTRA_ENVIRONMENT_ID
    });

    Settings: cpuCount, memoryMb and idleTimeoutMinutes. Unset values fall back to the defaults option, then to 2 CPUs, 1024 MB and 5 minutes. An unset idle timeout also honors the top-level idleTimeoutMinutes option before the built-in 5 minutes.

    Builds: starts a template build through the platform build API and reports its status. Builds are tracked in the host process, so after a restart an earlier build id reads as unknown.

@mastra/playground-ui@63.0.0

Minor Changes

  • Dialog now opens as a bottom drawer on phone-sized screens (below 640px), so every existing dialog adapts to mobile without changes at the call site. People can swipe it down to close, the same as pressing Cancel. Pending dialogs still refuse to close. (#26765)

    Removed props. The same dialog can now render either way, so DialogContent no longer accepts render or a function style, and Dialog no longer accepts handle. Pass a plain style object, and control the dialog with open instead of a handle:

    // Before
    const handle = DialogPrimitive.createHandle();
    <Dialog handle={handle}>
      <DialogContent style={state => ({ maxWidth: state.open ? 480 : 400 })}>…</DialogContent>
    </Dialog>;
    
    // After
    const [open, setOpen] = useState(false);
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogContent style={{ maxWidth: 480 }}>…</DialogContent>
    </Dialog>;
  • AlertDialog now opens as a bottom drawer on phone-sized screens (below 640px), the same as Dialog. Tapping outside still does not close it; swiping it down works like Cancel. Existing alert dialogs need no change. (#26768)

    Removed props. AlertDialog.Content no longer accepts render, a function style or role (it is always alertdialog), and AlertDialog no longer accepts handle. Pass a plain style object, and control the alert with open instead of a handle:

    // Before
    const handle = AlertDialogPrimitive.createHandle();
    <AlertDialog handle={handle}>
      <AlertDialog.Content style={state => ({ maxWidth: state.open ? 480 : 400 })}>…</AlertDialog.Content>
    </AlertDialog>;
    
    // After
    const [open, setOpen] = useState(false);
    <AlertDialog open={open} onOpenChange={setOpen}>
      <AlertDialog.Content style={{ maxWidth: 480 }}>…</AlertDialog.Content>
    </AlertDialog>;

    DialogContent with showCloseButton={false} no longer reserves space for the missing close button next to the header.

  • Added composable parts to Avatar: Avatar.Image, Avatar.Fallback, Avatar.Badge, Avatar.Group, and Avatar.GroupCount. Added xs and control sizes. Existing <Avatar name="…" /> usage is unchanged. (#26619)

    import { Avatar } from '@mastra/playground-ui/components/Avatar';
    
    <Avatar size="md">
      <Avatar.Image src={user.avatarUrl} alt={user.name} />
      <Avatar.Fallback>AM</Avatar.Fallback>
      <Avatar.Badge>
        <Avatar name={organization.name} src={organization.logo} />
      </Avatar.Badge>
    </Avatar>
    
    <Avatar.Group size="sm">
      <Avatar name="Jane Smith" src={jane} />
      <Avatar name="Sam Lee" src={sam} />
      <Avatar.GroupCount>+3</Avatar.GroupCount>
    </Avatar.Group>

    Avatar.Badge takes a tone (neutral, success, warning, destructive, info) for a status dot, or any child such as an icon or a small organization avatar. Avatars inside a group or badge take their size from it.

  • Added variant="inset" to Table from @mastra/playground-ui/components/Table. It gives a native table the same raised look as DataList, with rows that grow with their content and cells that can hold any component. Existing default and small tables render exactly as before. (#26625)

    The inset variant comes with composable parts:

    • Table.RowLink and Table.RowButton make a whole row a link or a button, and other controls in the row keep their own clicks.
    • Table.SortButton turns a header into a sort control with a direction arrow.
    • Table.Pagination adds numbered pages below the table; the page and rows stay in your code.
    • columnDividers draws lines between columns.
    import { Table } from '@mastra/playground-ui/components/Table';
    
    <Table variant="inset" aria-label="Credit history">
      <Table.Header>
        <Table.Row>
          <Table.Head>Type</Table.Head>
          <Table.Head align="end">Balance</Table.Head>
        </Table.Row>
      </Table.Header>
      <Table.Body>
        <Table.Row>
          <Table.Cell>
            <Table.RowLink href="/credits/1">Credit grant</Table.RowLink>
          </Table.Cell>
          <Table.Cell align="end">$6.95</Table.Cell>
        </Table.Row>
      </Table.Body>
    </Table>;
  • Added Pagination at @mastra/playground-ui/components/Pagination: page navigation built from parts (Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationPrevious, PaginationNext, PaginationEllipsis), plus getPaginationRange for choosing which page numbers to show. The current page slides between pages, and links can render as anchors or router links. (#26625)

    import {
      Pagination,
      PaginationContent,
      PaginationItem,
      PaginationLink,
    } from '@mastra/playground-ui/components/Pagination';
    
    <Pagination>
      <PaginationContent>
        <PaginationItem>
          <PaginationLink isActive>1</PaginationLink>
        </PaginationItem>
        <PaginationItem>
          <PaginationLink onClick={() => setPage(1)}>2</PaginationLink>
        </PaginationItem>
      </PaginationContent>
    </Pagination>;
  • Added a size prop to ProductAvatar and made it work inside Avatar.Group. In a group it takes the group's size and gets the same overlap ring as other avatars. Standalone ProductAvatar keeps its current size. (#26889)

    <Avatar.Group size="md">
      <ProductAvatar product="studio" />
      <ProductAvatar product="observability" />
      <Avatar.GroupCount>+3</Avatar.GroupCount>
    </Avatar.Group>
  • Added an inline variant to DataPanel that renders the panel in the normal page flow instead of a modal side drawer. Use it to embed detail views directly in a layout (no backdrop, focus trap, or dismissal). The parent controls mounting and size. (#26867)

    <DataPanel variant="inline" title="Span details">
      <DataPanel.Header>
        <DataPanel.Heading>Span details</DataPanel.Heading>
      </DataPanel.Header>
      <DataPanel.Content>…</DataPanel.Content>
    </DataPanel>

Patch Changes

  • Added a "Go to dataset" button to the toast shown after saving a trace, span or score as a dataset item. It opens the new item in its dataset. (#26866)

  • Improved the main content layout on narrow screens. Where the sidebar collapses into the mobile header, the content is now flat and runs edge to edge under the header instead of sitting in an inset, rounded card. Wider screens with the sidebar beside the content are unchanged. (#26858)

  • Fixed DataPanel headers on narrow screens. Header actions now wrap onto a second row instead of being pushed out of view, so every action, including the close button, stays reachable. (#26868)

  • Fixed the link button variant so it is always a link. Button variant="link" now renders an <a> with an href even when no link element is passed, instead of a button that only looked like one. A disabled link keeps its href and is marked aria-disabled. (#26817)

  • Fixed PageHeader shifting between pages. The title row is now 32px tall with the title in the same position whether or not the header has an icon or PageHeader.Action, so moving between pages no longer moves the title. PageLayout also reserves the scrollbar gutter in its scroll area, so a narrow, centered column no longer shifts sideways when a page starts to scroll. (#26915)

  • Fixed the filter bar adding a second chip for a field that already has one. On pages that keep one filter per field (Logs, Metrics, Traces), picking another value for an already filtered field now updates its chip instead of adding a chip that is silently ignored: values combine into "is any of" when the field supports it, otherwise the new value replaces the old one. (#26513)

    The Traces page also keeps "is any of" filters on Status and Primitive type in the URL. Before, they were dropped and the list stayed unfiltered.

    Operators declare their multi-value form with the new widensTo option:

    const operators = [
      { id: 'is', label: 'is', widensTo: 'in' },
      { id: 'in', label: 'is any of', arity: 'many' },
    ];
  • The inline DataPanel now uses the same rounded corners as the app frame, so it lines up with the main card when it sits next to it. The drawer keeps its current corners. (#26887)

  • Trace Intelligence now labels its list by Agent instead of Entity. Agents whose traces are still being analyzed show an "Analyzing" status and can no longer be clicked, since they have no signals to open yet. Hovering or focusing the status shows how many traces have been collected. (#26812)

@mastra/posthog@1.4.1

Patch Changes

  • The $ai_trace event of a served MCP request now includes callerTraceparent and callerTracestate, so you can see which trace called the MCP server. (#26059)

@mastra/react@1.10.0

Minor Changes

  • useCompareExperiments now compares two to ten experiments at once. Pass experimentIds (and optionally baselineId) instead of experimentIdA and experimentIdB. The options parameter, which no longer had any fields, is removed. (#26725)

    // Before
    useCompareExperiments({ datasetId, experimentIdA: 'exp-1', experimentIdB: 'exp-2' });
    
    // After
    useCompareExperiments({ datasetId, experimentIds: ['exp-1', 'exp-2', 'exp-3'], baselineId: 'exp-1' });

    Added hooks that load several experiments in parallel: useDatasetExperimentsQueries, useAllDatasetExperimentResultsQueries (every page of results, not just the first) and useScoresByExperimentIdsQueries. Each returns its data keyed by experiment ID and keeps polling experiments that are still running.

  • Added useExperimentsItemUsage, which returns the token usage and estimated cost of one or more experiments, keyed by experiment ID. Each experiment comes with its totals (summary) and the usage of every item, keyed by trace ID (traces). It sends one request per experiment, so it does not need the experiment results first, and keeps polling experiments that are still running. (#26853)

    const { data, isLoading } = useExperimentsItemUsage({
      experiments: [
        { id: 'exp-1', status: 'completed' },
        { id: 'exp-2', status: 'running' },
      ],
      supportsMetrics: true,
    });
    
    const total = data['exp-1']?.summary;
    const item = data['exp-1']?.traces[result.traceId];
    // both: { inputTokens, outputTokens, estimatedCost, costUnit }

    estimatedCost and costUnit are undefined when no price is known or when cost units are mixed.

@mastra/redis@1.4.10

Patch Changes

  • Added run fencing for durable agents. Redis stores now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26422)

    Claims are stored as keys in the same database, so no setup is needed.

    Claims and memory fences expire on their own: a claim 7 days after its last claim, renewal or release, and a fence 7 days after it is retired. Running executions renew their claim every few seconds, and fences of unfinished runs never expire. storage.prune() doesn't manage these keys, and the 7 days aren't configurable.

    Requires @mastra/core 1.76.0 or a later 1.x release.

@mastra/sentry@1.2.26

Patch Changes

  • Span links are now sent to Sentry as span links. (#26059)

@mastra/server@1.77.0

Minor Changes

  • The compare experiments route (POST /api/datasets/:datasetId/compare) now compares two to ten experiments at once. Send experimentIds and an optional baselineId: (#26725)

    { "experimentIds": ["exp-1", "exp-2", "exp-3"], "baselineId": "exp-1" }

    The previous { "experimentIdA", "experimentIdB" } body is still accepted and compares those two experiments with experimentIdA as the baseline. It is deprecated in favor of experimentIds.

Patch Changes

  • Forward authenticated users as AgentController message authors. (#26549)

  • Added an optional run id to tool-suspension responses so callers can target the exact suspended run when tool-call ids are reused. (#26058)

  • Workflow HTTP routes and POST /schedules no longer resolve internal workflows (such as a durable agent's durable-agentic-loop or the notification dispatcher). These now return 404, so agent runs can't be started or resumed through /workflows/* in a way that skips the agent routes' scope, ownership and approval checks. (#26940)

    If you called a durable agent's loop through the workflow routes, use the agent routes instead:

    - POST /workflows/durable-agentic-loop/start-async
    + POST /agents/:agentId/stream
    
    - POST /workflows/durable-agentic-loop/resume-async
    + POST /agents/:agentId/resume-stream
  • Fixed losing concurrent tool approvals and agent resumes returning HTTP 500. The approve, decline, and resume routes now return 409 Conflict when another request is already resuming the same run, or when the run isn't waiting to be resumed. (#26688)

@mastra/spanner@1.9.4

Patch Changes

  • Added run fencing for durable agents. Spanner stores now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26421)

    Claims are kept in two new tables, mastra_workflow_run_owners and mastra_memory_run_fences, which are created on init. If you set disableInit: true, create them before upgrading. Durable agent runs fail to start until the tables exist.

    storage.prune() doesn't cover these tables on Spanner yet, so their rows are kept.

    Requires @mastra/core 1.76.0 or a later 1.x release.

@mastra/upstash@1.5.3

Patch Changes

  • Added run fencing for durable agents. Upstash stores now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26422)

    Claims are stored as keys in the same database, so no setup is needed.

    Claims and memory fences expire on their own: a claim 7 days after its last claim, renewal or release, and a fence 7 days after it is retired. Running executions renew their claim every few seconds, and fences of unfinished runs never expire. storage.prune() doesn't manage these keys, and the 7 days aren't configurable.

    Requires @mastra/core 1.76.0 or a later 1.x release.

@mastra/valkey@0.2.6

Patch Changes

  • Added run fencing for durable agents. Valkey stores now reject writes from a durable agent execution that lost its run to crash recovery, so it can't overwrite the recovered run's messages, threads, or workflow state (#23734). (#26422)

    Claims are stored as keys in the same database, so no setup is needed.

    Claims and memory fences expire on their own: a claim 7 days after its last claim, renewal or release, and a fence 7 days after it is retired. Running executions renew their claim every few seconds, and fences of unfinished runs never expire. storage.prune() doesn't manage these keys, and the 7 days aren't configurable.

    Requires @mastra/core 1.76.0 or a later 1.x release.

Other updated packages

The following packages were updated with dependency changes only:

Don't miss a new mastra release

NewReleases is sending notifications on new releases.