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/serverworkflow 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, andSessionSuspensions.resolveToolCallId(). @mastra/playground-ui:Dialog/AlertDialogremoved handle/render-style props; components are now controlled viaopen/onOpenChange.@mastra/client-js/@mastra/reactcompare-experiments APIs deprecateexperimentIdA/experimentIdBin favor ofexperimentIds(+ optionalbaselineId).
Changelog
@mastra/core@1.77.0
Minor Changes
-
AgentController
steerandfollowUpnow accept file attachments, matchingsendMessage. 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
/steerand/follow-uproutes accept the samefilesfield 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 oneMastraSandboxper session still works, but it gives a provider no way to expose tunable settings, a prebuilt repository template or template builds. AFactorySandboxowns all four, and lives in@mastra/core/workspaceso provider packages implement it without depending on@mastra/factory. (#26467)Required:
providerandcreate, which returns the session'sMastraSandbox.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);templatereturns the environment's repository template for a context and settings;buildsstarts, 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
authortosendMessage,queueMessage, orsteer, and read it back withgetMessageAuthor. (#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
pricingfield 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
linksto spans: references to spans in other traces. Passlinkswhen 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()andformatTraceparent()to@mastra/core/observabilityfor W3Ctraceparentvalues. -
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:
commitActiveObservationswrites active observations and returns{ applied, reason }. It acceptsexpectedActiveObservationsand never writes to a superseded generation.appendBufferedObservationsappends a buffered chunk to the current generation and returns{ persisted, recordId }.createReflectionGenerationandswapBufferedReflectionToActiveacceptnewRecordId; the reflection applied when the returned record has that id.swapBufferedToActivereportsretired.- 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. }
updateActiveObservationsandupdateBufferedObservationskeep returningPromise<void>. In stores that implement the new methods,updateActiveObservationsnow throws when the observations were not written (the generation was superseded, orexpectedActiveObservationsno longer matches) instead of resolving as if they had been. An older@mastra/memoryrunning 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,
onFinishnow receives the usage of steps that completed before the abort:totalUsageis the sum across those steps andusageis 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,usageandtotalUsagestill have undefined token counts. (#26629) -
Update provider registry and model documentation with latest models and providers (
eaa9d12) -
Added an optional
project_id2to server feature and model-usage telemetry while preserving existing project identifiers and usage cursors. UUID-shapedMASTRA_PROJECT_IDvalues are sent unhashed with anmp_prefix for direct Platform lookup. Other values fall back to a hash of the normalized Gitoriginremote, 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=1to 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 withAGENT_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_CLAIMEDid.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.statusgives its state.AGENT_RESUME_NO_SNAPSHOT_FOUND: there is no record of a suspended run. Check therunId. 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.stepsandoutput.responsenow include each step's response messages and model id, the same as a regular agent. Amodelpassed to a durable agent'sstream()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.messageanderror.stackinstead of an empty object. Added exportedAgentExperimentOutput,ScorerExperimentOutput, andExperimentResultOutputtypes that describe what each experiment target stores inoutput. (#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 withsavePerStep: true. The user's message, earlier turns, and messages sent in as input are kept. Fixes #26215. (#26252) -
Added
targetTypeto experiment results. The resultoutputis now typed based on the target, so checkingtargetTypegives 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) keepoutputasunknown.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
writerstream only when your code readswriter. 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 callwriter.write()orwriter.custom()behave as before. (#26665) -
Fixed
mastra.datasets.compareExperiments()silently comparing only the baseline and one other experiment. Every ID inexperimentIdsnow gets a result entry on each item (nullwhen that experiment did not run the item). Duplicate IDs are ignored, and passing abaselineIdthat isn't one ofexperimentIdsnow 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 foundwhen an orchestration worker runs steps on the API server throughMASTRA_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 asToolSearchProcessor's loaded tools) running with an emptycontext.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). Toolexecutefunctions now receive the transformed output type instead of a union of input and output types. ToolrequestContextSchemanow 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
StorageMCPClientToolsConfigandStoredMCPClientToolsConfigJSDoc. It previously said that omittingtoolsincludes all MCP tools. For an agent-level MCP client reference, omittingtoolsselects no tools. Usetools: {}to include all tools. (#26738)Also in: @mastra/client-js@1.54.0
-
ExpectedStepnow supportsprovider_tool_callsteps, so you can write typed trajectory expectations for provider-executed tools such as web search. The trajectory scorer compares theirtoolArgs,toolResult, andsuccessfields. (#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
outputcould miss every tool call made beforesuspend(), or end up empty. Scorers now receive every response message the run produced, across all suspend/resume segments, under the originalrunId. 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.precedesMessageIdto 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.blockAfterdocumentation for Observational Memory: betweenmessageTokensandblockAfteronly background buffering and activation run, and a blocking observation runs at or aboveblockAfterwhen no buffered chunk activates. (#22078) -
Fixed
structuredOutputso 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.objectnow 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()andsession.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 anaddressthat can be passed tosession.respondToToolSuspension({ address, resumeData }). ExportedSuspensionAddressandgetSuspensionAddressKeyfor hosts that persist or key those addresses.Deprecated the
SessionRecord.modeId,modelId,state, andpendingfields, theHarnessStorage.appendPendingItem(),updatePendingItem(), andremovePendingItem()helpers, andSessionSuspensions.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()andshutdown()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_settingstoObservabilityStorageClickhouseVNextand the other ClickHouse storage domains, so settings such asfilesystem_prefetches_limitcan 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. PassexperimentIdsand optionallybaselineId: (#26725)await client.compareExperiments({ datasetId, experimentIds: ['exp-1', 'exp-2', 'exp-3'], baselineId: 'exp-1' });
experimentIdA/experimentIdBstill work for comparing two experiments, but are deprecated in favor ofexperimentIds.
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_ownersandmastra_memory_run_fences, which are created on init. If you setdisableInit: true, create them before upgrading. Durable agent runs fail to start until the tables exist.CloudflareKVStoragedoesn't support run fencing.storage.prune()doesn't cover these tables on Durable Objects yet, so their rows are kept.Requires
@mastra/core1.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_ownersandmastra_memory_run_fences, which are created on init. If you setdisableInit: 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/core1.76.0 or a later 1.x release.
@mastra/code-sdk@1.13.0
Minor Changes
-
The
themepreference now accepts any theme name, not justauto,dark, orlight, so callers can persist bundled and custom theme selections. (#26719) -
Added the
instructionRootssession state field. A host that checks out several repositories under one working directory lists their directories, and the system prompt loads each one'sAGENTS.mdorCLAUDE.mdin 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 asettled()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-levelerrorsmaps, 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_itemsreturned as either an array or a newline-joined string. (#26516) -
@mastra/connectnow requires@mastra/core1.76.0 or later, matching the@mastra/mcpversion 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_documentstable, 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/core1.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
supersededByfield; 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()andshutdown()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 callsprocess.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 newflushGraceMsoption (default 2000 ms,0disables): (#26761)new DatadogExporter({ mlApp: 'my-app', flushGraceMs: 3000 });
-
Span links are now sent to Datadog as span links, from both
DatadogExporterandDatadogBridge. (#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 asDockerSandboxexceptidandtemplate. (#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, andowner, theuser[:group]that owns the checked-out repositories in it. Unset values fall back to thedefaultsoption, then tonode:22-slimand 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_ownersandmastra_memory_run_fences, which are created on init. If you setdisableInit: 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/core1.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/core1.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,memoryMbandidleTimeoutMinutes. Unset values fall back to thedefaultsoption, 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
buildscapability, Factory builds the environment template ahead of sessions instead of on the first session boot. (#26479)POST /web/factory/projects/:id/environment/buildstarts a build now (the Environment page's Build now button, ormastra api factory project environment build <project-id>).GET /web/factory/projects/:id/environment/buildsandGET /web/factory/projects/:id/environment/builds/:buildIdread 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 intonew Mastrapick up thefactory-environment-buildworkflow. - After a session's own setup, each repository link records
configuredorfailedwith 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-environmentstate 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.mdorCLAUDE.mdand 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
FactorySandboxsupport toMastraFactoryConfig.sandbox. Pass a provider instance (PlatformFactorySandbox,E2BFactorySandbox,DockerFactorySandbox, or the newLocalFactorySandboxexported from@mastra/factory) and the factory gains that provider's environment settings and template builds. The callback form still works and is wrapped as aprovider: '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,
GETandPATCH /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,
nullremoves a key, and the result is checked against the provider's schema before anything is written. A rejected value answers 400invalid_environmentnaming the field; a factory without a sandbox answers 400no_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 exceptsource_control_refresh_change_request_checkout(the GitLab checkout refresh) takes an optionalrepositoryslug; 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 createobserver 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_prand pull request subscriptions are gated on the Factory, so web user sessions get them too.- On the platform GitHub provider,
GH_TOKENcovers 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 thesource_control_*tools for the others.
For example, an agent in a session filed under
acme/apipushes and opens a change request inacme/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" } } - Every
-
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.createon 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.
- List every session of a Factory the caller can see, across all source-control providers, with
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/environmentnow 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: nullorMASTRACODE_AUTH_DISABLED=1).GET /auth/menow 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
secretEncryptionon 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.mdwith invalid YAML (for example an unquoted:in thedescription) 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
modelScopefield to Factory projects, defaulting tofactoryto 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
FactoryProjecttype: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
.envare never listed. - The agent's file tools and project path start at the workspace root, and each repository's
AGENTS.mdand 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
stepIndexandisContinuedon 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
spanTypeequalsmodel_stepandisContinuedequals"false"to judge only the final step of each agent run. Attributes that hold objects or arrays, such as workflowentryMetadata, 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 usesmanualFlushMode(overlapping calls wait for the earlier upload), andshutdown()sends the updates for spans it aborts instead of leaving them pending. (#26760)
@mastra/libsql@1.26.1
Patch Changes
-
Fixed
LibSQLFactoryStorage.withTransactionon: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 lockederrors when observational memory and message saves run at the same time on a local file database. - With Turso embedded replicas (
urlplussyncUrl), 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 prebuiltclient.
Upgrading. A nullable
supersededBycolumn 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: trueorMASTRA_DISABLE_STORAGE_INIT=true), startup does not run this migration. Add a nullablesupersededBycolumn, of the same type asid, tomastra_observational_memorybefore you upgrade, or reflections and buffered activation fail with a missing-column error.
@mastra/loggers@1.3.7
Patch Changes
-
Fixed
HttpTransportandUpstashTransportmodifying log objects passed to them. When a record has no timestamp, the transport now addstimeto its own copy, so the caller's object stays unchanged and frozen records are sent instead of failing with aTypeError. (#26892) -
Fixed
UpstashTransportreturning 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 W3Ctraceparentof the tool call span.MCPServerkeeps its own trace for the request and links its request span to that span. Itstools/callreply names the span that served the call, andMCPClientlinks 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
traceparentandtracestatefrom the request_meta, or from the HTTP headers that OpenTelemetry-instrumented callers send. It also records them on the request span ascallerTraceparentandcallerTracestate, so exporters without span link support still show the caller.This needs no setup. To send your own trace context, or none, set
traceContexton the server:const mcp = new MCPClient({ servers: { orders: { url: new URL('https://orders.example.com/mcp'), traceContext: () => undefined, // send no trace context }, }, });
@mastra/mcpnow requires@mastra/core1.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
messageTokenswith async buffering enabled. BetweenmessageTokensandobservation.blockAfter(default1.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 reachblockAfterafter 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
blockAfterbefore a step blocks. To keep the previous behavior of observing synchronously at the threshold, setblockAfter: 1:observationalMemory: { observation: { messageTokens: 30_000, blockAfter: 1 }, }
A multiplier
blockAfternow scales a per-threadmessageTokensoverride instead of the instance threshold. WithblockAfter: 1.2, this thread blocks at 12,000 tokens:await memory.updateObservationalMemoryConfig({ threadId, config: { observation: { messageTokens: 10_000 } }, });
getStatus()also returnsobservationBlockAfterandinAsyncObservationBand.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.previousObserverTokensbeing 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: 0omits previous observations. The default is nowfalse, 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
Memoryinstances 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.instructiontext 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
supersededByfield 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 uniqueidindex on the observational memory collection, which is created by default; if you useskipDefaultIndexes, 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.runOwnershipandretention.memory.runFencesand callstorage.prune().Requires
@mastra/core1.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 thanmaxAgewhen 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_ownersandmastra_memory_run_fences, which are created on init. If you setdisableInit: 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/core1.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
supersededBycolumn 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: trueorMASTRA_DISABLE_STORAGE_INIT=true), startup does not run this migration. Add a nullablesupersededBycolumn, of the same type asid, tomastra_observational_memorybefore 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_ownersandmastra_memory_run_fences, which are created on init. If you setdisableInit: 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/core1.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
pricinga custom gateway returns fromfetchProviders(). Prices load on the first call that has no price, and also when Mastra starts undermastra devor withMASTRA_AUTO_REFRESH_PRICING=true. A gateway that fails to load is logged and retried about once an hour. SetMASTRA_OFFLINE=trueto skip loading them. (#26875) -
Cost estimates now use model prices from models.dev. Each release of
@mastra/observabilityincludes a recent snapshot of these prices. (#26518)Models that models.dev no longer lists, mostly retired ones such as
claude-3-7-sonnet,o1-miniandgemini-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. SetMASTRA_AUTO_REFRESH_PRICING=falseto turn it off. - In other environments, such as production, refresh is off by default. Set
MASTRA_AUTO_REFRESH_PRICING=trueto turn it on. MASTRA_OFFLINE=trueturns refresh off everywhere, whateverMASTRA_AUTO_REFRESH_PRICINGis set to.
The
modellabel 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. - Under
Patch Changes
-
Update bundled model pricing with the latest prices from models.dev (
6e5f3b3) -
Spans now keep and export the
linksthey 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) -
MastraStorageExporterandDefaultExporterflush()andshutdown()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_ownersandmastra_memory_run_fences, which are created on init. If you setdisableInit: 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/core1.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
supersededBycolumn 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: trueorMASTRA_DISABLE_STORAGE_INIT=true), startup does not run this migration. Add a nullablesupersededBycolumn, of the same type asid, tomastra_observational_memorybefore 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()andshutdown()now flush the global tracer and logger providers registered byNodeSDK, 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
lastBufferedAtTimebeing read back shifted by the host's time-zone offset. (#25906)Upgrading. A nullable
supersededBycolumn 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: trueorMASTRA_DISABLE_STORAGE_INIT=true), startup does not run this migration. Add a nullablesupersededBycolumn, of the same type asid, tomastra_observational_memorybefore 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 asPlatformSandboxexcept 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,memoryMbandidleTimeoutMinutes. Unset values fall back to thedefaultsoption, then to 2 CPUs, 1024 MB and 5 minutes. An unset idle timeout also honors the top-levelidleTimeoutMinutesoption 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
-
Dialognow 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
DialogContentno longer acceptsrenderor a functionstyle, andDialogno longer acceptshandle. Pass a plain style object, and control the dialog withopeninstead 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>;
-
AlertDialognow opens as a bottom drawer on phone-sized screens (below 640px), the same asDialog. Tapping outside still does not close it; swiping it down works like Cancel. Existing alert dialogs need no change. (#26768)Removed props.
AlertDialog.Contentno longer acceptsrender, a functionstyleorrole(it is alwaysalertdialog), andAlertDialogno longer acceptshandle. Pass a plain style object, and control the alert withopeninstead 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>;
DialogContentwithshowCloseButton={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, andAvatar.GroupCount. Addedxsandcontrolsizes. 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.Badgetakes atone(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"toTablefrom@mastra/playground-ui/components/Table. It gives a native table the same raised look asDataList, 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.RowLinkandTable.RowButtonmake a whole row a link or a button, and other controls in the row keep their own clicks.Table.SortButtonturns a header into a sort control with a direction arrow.Table.Paginationadds numbered pages below the table; the page and rows stay in your code.columnDividersdraws 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
Paginationat@mastra/playground-ui/components/Pagination: page navigation built from parts (Pagination,PaginationContent,PaginationItem,PaginationLink,PaginationPrevious,PaginationNext,PaginationEllipsis), plusgetPaginationRangefor 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
sizeprop toProductAvatarand made it work insideAvatar.Group. In a group it takes the group's size and gets the same overlap ring as other avatars. StandaloneProductAvatarkeeps its current size. (#26889)<Avatar.Group size="md"> <ProductAvatar product="studio" /> <ProductAvatar product="observability" /> <Avatar.GroupCount>+3</Avatar.GroupCount> </Avatar.Group>
-
Added an
inlinevariant toDataPanelthat 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
linkbutton 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 markedaria-disabled. (#26817) -
Fixed
PageHeadershifting between pages. The title row is now 32px tall with the title in the same position whether or not the header has an icon orPageHeader.Action, so moving between pages no longer moves the title.PageLayoutalso 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
widensTooption:const operators = [ { id: 'is', label: 'is', widensTo: 'in' }, { id: 'in', label: 'is any of', arity: 'many' }, ];
-
The inline
DataPanelnow 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_traceevent of a served MCP request now includescallerTraceparentandcallerTracestate, so you can see which trace called the MCP server. (#26059)
@mastra/react@1.10.0
Minor Changes
-
useCompareExperimentsnow compares two to ten experiments at once. PassexperimentIds(and optionallybaselineId) instead ofexperimentIdAandexperimentIdB. Theoptionsparameter, 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) anduseScoresByExperimentIdsQueries. 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 }
estimatedCostandcostUnitareundefinedwhen 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/core1.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. SendexperimentIdsand an optionalbaselineId: (#26725){ "experimentIds": ["exp-1", "exp-2", "exp-3"], "baselineId": "exp-1" }The previous
{ "experimentIdA", "experimentIdB" }body is still accepted and compares those two experiments withexperimentIdAas the baseline. It is deprecated in favor ofexperimentIds.
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 /schedulesno longer resolve internal workflows (such as a durable agent'sdurable-agentic-loopor 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_ownersandmastra_memory_run_fences, which are created on init. If you setdisableInit: 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/core1.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/core1.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/core1.76.0 or a later 1.x release.
Other updated packages
The following packages were updated with dependency changes only:
- @mastra/agent-builder@1.1.28
- @mastra/arize@1.3.24
- @mastra/arthur@0.4.24
- @mastra/deepeval@0.1.18
- @mastra/deployer@1.77.0
- @mastra/deployer-cloud@1.77.0
- @mastra/deployer-cloudflare@1.2.36
- @mastra/deployer-netlify@1.2.36
- @mastra/deployer-sandbox@0.3.21
- @mastra/deployer-vercel@1.2.36
- @mastra/e2b-desktop@0.1.7
- @mastra/editor@0.16.2
- @mastra/elysia@0.1.17
- @mastra/express@1.5.21
- @mastra/fastify@1.5.21
- @mastra/hono@1.7.19
- @mastra/koa@1.7.21
- @mastra/laminar@1.3.26
- @mastra/longmemeval@1.1.36
- @mastra/mcp-docs-server@1.3.7
- @mastra/nestjs@0.2.36
- @mastra/opencode@0.1.36
- @mastra/temporal@0.4.15
- @mastra/turso@0.1.15
- @mastra/voice-google-gemini-live@0.14.15
- @mastra/voice-openai-realtime@0.14.5
- @mastra/voice-xai-realtime@0.2.14