Highlights
File-based Agent Schedules
File-based agents can now declare recurring tasks via a schedules/ directory (TS or Markdown) that syncs to schedule storage at startup, eliminating runtime registration code and making schedule IDs stable across builds.
New Storage Provider: @mastra/oracledb
Added @mastra/oracledb with OracleStore (composite storage across Mastra domains) and OracleVector (Oracle 23ai+ VECTOR columns with metadata filtering), enabling Oracle Database-backed deployments.
QuickJS In-Process Code Mode Transport (@mastra/quickjs)
New @mastra/quickjs transport runs model-authored programs in an in-process QuickJS WASM runtime (no native addon, no --no-node-snapshot), enabling Code Mode on more serverless hosts without a full sandbox.
Dynamic Workflows: Workflow Builder API + Code SDK Management
Dynamic workflows gain a reusable Workflow Builder authoring API (createWorkflowBuilderAgent) plus Mastra Code SDK support for discovery-backed creation, persistence, execution, and deletion of workflows.
Custom createRoute() API Routes Across Server + Adapters
createRoute() routes can now be registered via server.apiRoutes with native validation/auth/streaming/OpenAPI support on @mastra/server, and are supported in server adapters (Hono/Express/Fastify/Koa) for consistent typed routing.
Breaking Changes
- Stored workflows renamed to dynamic workflows:
addStoredWorkflow(s)→addDynamicWorkflow(s),StoredWorkflow*types →DynamicWorkflow*, andworkflow.originnow reports'dynamic'. - Channel agent replies now post as markdown by default; set
textFormat: 'plain'on channel adapter configs (e.g. Slack) to preserve literal plain-text posting.
Changelog
@mastra/core@1.58.0
Minor Changes
-
Add
resolveSessionandonStaleToolApprovalto agent controller channels. (#21094)resolveSessioncreates the session for a mapped channel thread in place of the built-in call. It runs before any session exists, so a host can refuse a request before a session, a model call, or any output happens, which is somethingonSessionStartcannot do, because it runs after the session is created and swallows errors. A refusal is silent: the chat thread gets nothing, so the host's authorization message never lands in a shared channel. Other failures still post an error to the thread as before, and a refusal is distinguishable asChannelSessionRejectedErrorwith the original error as itscause.Approval actions now resolve their session with the action's own request context, so a shared install can revalidate the person answering an approval instead of trusting the request that opened the session.
onStaleToolApprovalreports approval actions that have no matching parked gate, which is every approval answered after a restart. Mastra still refuses to run the tool; the hook lets a durable host settle that attempt rather than dropping the answer. It receives therunIdthe approval card was rendered for: the attempt the user answered, plus the session'scurrentRunId, which after a restart is usually null or a different run. -
Added typed experiment provenance and grouping fields that persist across synchronous and asynchronous runs and can be filtered when listing experiments. (#20645)
await dataset.startExperiment({ task, scorers, provenance: { source: 'github', sourceVersion: 'abc123' }, grouping: { experimentSetId: 'benchmark-1', variantId: 'candidate', trialIndex: 0 }, });
-
Added
instructions.tsas a code alternative toinstructions.mdfor file-based agents. Use it when the prompt needs TypeScript, for example when it's built from shared constants or resolved per request. (#20847)Before, a computed prompt had to move into
config.ts, splitting it away from the agent's other instructions:// src/mastra/agents/support/config.ts import { agentConfig } from '@mastra/core/agent'; export default agentConfig({ model: 'openai/gpt-5.6-sol', instructions: ({ requestContext }) => { const tier = requestContext.get('tier') ?? 'standard'; return `You are a support agent. Treat this as a ${tier}-tier customer.`; }, });
Now it lives in its own file, next to
config.ts:// src/mastra/agents/support/instructions.ts import { agentInstructions } from '@mastra/core/agent'; export default agentInstructions(({ requestContext }) => { const tier = requestContext.get('tier') ?? 'standard'; return `You are a support agent. Treat this as a ${tier}-tier customer.`; });
The file default-exports a string, a system message, or a function returning one.
agentInstructions()is an identity helper that only adds editor types.Precedence
A function
config.instructionsstill wins over both files, theninstructions.tswins overinstructions.md, which still wins over a staticconfig.instructions. Defining instructions in more than one place logs a warning naming both sources and which one wins.One upgrade case needs a rename. If an agent directory already holds an unrelated
instructions.ts, for example a helper thatconfig.tsimports, Mastra now reads that file as the agent's instructions instead of its old source, and the build fails if the file has no default export. -
Added AgentController live-session deletion with a process-local listener. Deletion is runtime-only: persisted threads and messages remain in storage and can be resumed by a future session. (#21174)
controller.onSessionDeleted(session => { console.log(session.identity.getResourceId()); }); await controller.deleteSession({ resourceId: 'project-42' });
-
Add
beforeAll,afterAll,beforeEach, andafterEachlifecycle hooks torunExperimentfor setting up and tearing down test data around an experiment run.beforeAllfailures fail the experiment,beforeEachfailures mark the item failed and skip execution (withafterEachstill running), andafterEachfailures are logged as warnings. (#21082) -
Added an
openIfEmptyoption to streamed tool display results. Set it to (#20909)
falsewhen a tool lifecycle chunk should update only an active streaming
session:toolDisplay: event => ({ kind: 'stream', chunk: createTaskUpdate(event), openIfEmpty: false, });
By default, stream results continue to open a session when needed. Static
channels continue to use plain-text fallback rendering. -
Added support for schema-aware routes created with
createRoute()inserver.apiRoutes. (#21184)const route = createRoute({ method: 'POST', path: '/items', responseType: 'json', bodySchema: z.object({ name: z.string() }), handler: async ({ name }) => ({ name }), }); const mastra = new Mastra({ server: { apiRoutes: [route] }, });
-
Added file-based schedules for agents (#20711)
File-based agents can now declare recurring tasks in a
schedules/directory next to their tools and skills. Mastra registers them into schedule storage at startup, so a scheduled agent no longer needs any runtime registration code.Each file is one schedule: a cron expression plus exactly one execution mode. Prompt mode runs the owning agent with a fixed message.
// src/mastra/agents/support/schedules/heartbeat.ts import { defineSchedule } from '@mastra/core/agent'; export default defineSchedule({ cron: '*/5 * * * *', prompt: 'Check system health and report any failures.', });
Handler mode computes the fire when it triggers, and returning
nullskips it.// src/mastra/agents/support/schedules/billing/sweep.ts import { defineSchedule } from '@mastra/core/agent'; export default defineSchedule({ cron: '0 3 * * *', handler: async () => { const overdue = await findOverdueInvoices(); if (overdue.length === 0) return null; return { prompt: `Chase ${overdue.length} overdue invoices.` }; }, });
A schedule's id is its path under
schedules/with the extension stripped, sobilling/sweep.tsbecomesbilling/sweepand stays stable across builds. Editing a cron patches the stored schedule and recomputes its next fire time, deleting the file deletes the schedule, and pausing a schedule through the API survives a redeploy. Schedules created withmastra.schedules.create(...)are in a separate namespace and are never touched by this sync.A schedule can also be a Markdown file, using cron frontmatter with the document body as the prompt.
// src/mastra/agents/support/schedules/cleanup.md --- cron: "0 3 * * *" --- Review tickets untouched for 30 days and close the ones that are resolved.Declaring a schedule is enough to start the scheduler. Schedules are supported on root agents only; a
schedules/directory undersubagents/fails the build, because the scheduler cannot resolve a subagent as a run target.defineScheduleis exported from both@mastra/core/agentand@mastra/core/schedules, so authoring a file-based agent needs a single import path.Build and dev support the same convention: schedules are discovered at build time, Markdown schedules fail the build with a message naming the file when the cron is missing, the body is empty, the frontmatter has an unknown field, or the YAML is unparseable. That last case has its own message because a leading
*is a YAML alias, socron: */5 * * * *needs quoting. The dev server rebuilds when a Markdown schedule changes. -
Added a
command_exitsession event to the agent controller. Subscribers now receive the exit code and success flag of each foregroundexecute_commandtool call, alongside the existingshell_outputstream: (#21211)session.subscribe(event => { if (event.type === 'command_exit') { console.log(event.toolCallId, event.exitCode, event.success); } });
Previously the exit outcome was only visible inside the tool result text, so observers could stream a command's output but never tell whether it succeeded.
-
Made the workspace optional when creating an agent controller session. Previously
createSession()threwA session requires a valid workspace instanceunless a workspace was configured, which blocked chat-style sessions that only need threads, state, and agent runs. (#21084)// Now works — no workspace configured anywhere const controller = new AgentController({ id: 'chat', storage, modes }); const session = await controller.createSession({ resourceId: 'user-1' }); session.getWorkspace(); // undefined
Session.getWorkspace()now returnsWorkspace | undefined, so check the result before using it. Passing a value that is not aWorkspaceinstance is still rejected. Sessions that do configure a workspace are unchanged, including workspace initialization and theworkspace_ready/workspace_errorevents.Closes #20594
-
Renamed the beta stored workflows feature to dynamic workflows.
mastra.addStoredWorkflow()is nowmastra.addDynamicWorkflow(),addStoredWorkflows()is nowaddDynamicWorkflows(), theStoredWorkflow*types are nowDynamicWorkflow*, andworkflow.originreports'dynamic'instead of'stored'. (#20938) -
Added a reusable Workflow Builder authoring API for discovering registered primitives, validating complete workflow definitions, and creating strict-provider-safe builder agents. Built-in workspace tools now expose explicit output schemas so workflow authors can infer tool compatibility. (#21210)
import { createWorkflowBuilderAgent } from '@mastra/core/workflows/builder'; const workflowBuilder = createWorkflowBuilderAgent({ id: 'workflow-builder', name: 'Workflow Builder', tools: discoveryAndSaveTools, model, });
-
Added a
requestContextKeysoption to scorer runs that controls which request-context values are recorded on the eval's span input for repeatability. (#20808)Previously the entire request context was recorded on every scorer-run span. Because request context is an arbitrary, app-controlled bag, that could persist secrets or PII stored under any key into exported traces and datasets. Scorer runs now record nothing from the request context by default — you opt in per key.
What changed
- Default (no
requestContextKeys): nothing from the request context is recorded. - Specific keys: only those keys are recorded, with dot notation for nested values.
['*']: the whole context is recorded (the framework-managed auth token stays redacted).
This is separate from the observability config's
requestContextKeys, which controls live span metadata. Recording a run so it can be reproduced later and surfacing keys on every live span are different concerns, so they are controlled independently.Example
await scorer.run({ input, output, requestContext: { userId: 'u_123', tenant: { id: 't_1', apiKey: 'secret' } }, // Persist only what you need to reproduce the run — `apiKey` is never stored. requestContextKeys: ['userId', 'tenant.id'], });
- Default (no
-
Added a delegated request context to
onDelegationStart. Each subagent run receives a context map derived from its parent, so hooks can add values before dynamic agent configuration resolves without changing the parent context. (#20853)await supervisor.stream('Research AI trends', { delegation: { onDelegationStart: context => { context.requestContext.set('specialty', context.primitiveId); }, }, });
Subagent request contexts inherit caller entries but exclude parent memory, thread, and resource identity. Setting or deleting entries during a subagent run no longer changes the parent context map or another concurrent delegation's map.
-
Added an opt-in
toolCallConcurrencystrategy that parallelizes safe tool calls when an approval or suspending tool is registered but not actually called in a step. (#21106)Previously, registering any tool that requires approval or can suspend forced every tool-call batch to run one at a time, even when the model never called that tool. You can now opt in to resolving concurrency from the tools the model actually called:
Before
// Any registered approval/suspend tool forced sequential execution const stream = await agent.stream('...', { toolCallConcurrency: 8 });
After
// A pure-safe batch runs in parallel; a batch that calls an approval/suspend tool still serializes const stream = await agent.stream('...', { toolCallConcurrency: { limit: 8, strategy: 'called' }, });
The default strategy ('available') keeps the existing conservative behavior. This works across both the standard loop and the durable engine. Closes #20100.
-
Added tracing to dynamic agent skills resolvers. The resolver now runs inside a
resolve-skillsspan, so skills fetched from an external service show up in the agent's trace instead of disappearing, and it receivestracingContextfor creating child spans of its own — the same pattern tools already use. (#20949)const agent = new Agent({ skills: async ({ requestContext, tracingContext }) => { const span = tracingContext?.currentSpan?.createChildSpan({ type: 'generic', name: 'entitlements-lookup', }); const skills = await fetchSkillsFor(requestContext.get('userId')); span?.end(); return skills; }, });
On metadata reads such as
agent.listSkills()no agent is running, sotracingContext.currentSpanisundefined— guard span usage with?.as shown above. -
Fixed agent streams closing silently when a provider ends the stream with
finishReason: 'error'but sends no error payload (for example, Google models reportingMALFORMED_FUNCTION_CALL). Previously the stream closed with no error part andonErrornever fired, so a failed run looked identical to a turn that produced no text. The stream now emits anerrorchunk and callsonError, and error processors can intercept and retry the failure. (#20302)Added
stepResult.rawReasontostep-finishandfinishchunks. It preserves the provider's own finish reason instead of collapsing it to'error', so you can tell distinct provider failures apart:for await (const chunk of stream.fullStream) { if (chunk.type === 'step-finish') { chunk.payload.stepResult.reason; // 'error' chunk.payload.stepResult.rawReason; // 'MALFORMED_FUNCTION_CALL' } }
These runs previously resolved as if they had succeeded with empty output. They now fail the same way a provider-reported error already did:
agent.generate()rejects, and so do awaited stream promises such asstream.text. IteratingfullStreamstill completes normally, with anerrorchunk included. -
Add
includeResolvedToolstoToolSearchProcessor, making per-request tools (MCP tools that need the caller's credentials, or anything returned by a dynamictoolsfunction) searchable and withholding them from the prompt until the agent loads them. Previously only tools listed at construction could be searched, so dynamically resolved tools were always sent in full. (#21016)const toolSearch = new ToolSearchProcessor({ tools: staticTools, includeResolvedTools: true, }); const agent = new Agent({ id: 'mcp-agent', name: 'mcp-agent', instructions: 'Search for a tool when you need a capability you do not have.', model: 'openai/gpt-5.6-sol', // Resolved per request, then searchable alongside staticTools tools: async ({ requestContext }) => mcpClient.getTools(requestContext.get('userToken')), inputProcessors: [toolSearch], });
-
Added an explicit A2A Protocol v1.0 SDK export while preserving the existing v0.3 export. (#20811)
import { ListTasksRequest } from '@mastra/core/a2a/v1'; const request = ListTasksRequest.fromJSON({ pageSize: 20 });
-
Set
tools.writeLockTimeoutMswhen a remote or cold-starting filesystem needs more than 30 seconds to accept a write. The write tool waits for the configured timeout before returning awrite-lock timeouterror. (#20809)const workspace = new Workspace({ filesystem: mySandboxFilesystem, tools: { // allow a cold-starting sandbox time to accept its first write writeLockTimeoutMs: 210_000, }, });
The default is unchanged at 30 000 ms.
-
Channel agent replies now post as markdown by default, so Slack renders bold text, links, and tables natively and other chat platforms convert the reply to their own format. Previously the final reply was posted as literal plain text, which made standard markdown show up as raw
**bold**and[title](url)characters in Slack while the same reply rendered correctly in Studio. (#20971)This is a behavior change for every channel agent. If your agent was prompted to emit a platform dialect such as Slack mrkdwn to work around the old behavior, either remove those prompt instructions (recommended) or set the new
textFormat: 'plain'option on the channel adapter config to keep posting literal plain text:channels: { adapters: { slack: { adapter: createSlackAdapter(), textFormat: 'plain', }, }, },
textFormatapplies to final reply text only. Tool cards, error messages, tripwire notices, and native streaming (which was already markdown) are unchanged. Postable channel messages now also accept a{ markdown: string }object alongside strings and card elements. -
Lightweight trace lists now work on every storage backend. Their rows carry an
inputPreviewso a list can render its preview column without transferring the whole prompt, plus a computedstatusand the spanmetadataso Studio's configurable trace columns work unchanged on the lightweight list. (#20677)const { spans } = await storage.listTracesLight({ pagination: { page: 0, perPage: 25 } }); spans[0].inputPreview; // short preview text — no input/output/attributes blobs
ObservabilityStorage.listTracesLight()previously threw on any backend that did not implement it — which was every backend except ClickHouse, DuckDB and the in-memory store. It now defaults tolistTraces()with each row projected down, so all backends serve the same response shape. Backends that can push the projection into the query should still override it; that is what keeps the blob columns off the read path.lightSpanRecordSchemagains optionalinputPreview,statusandmetadatafields, andbuildInputPreview()/toLightSpanRecord()are exported for stores that derive the preview at read time.listTracesLightResponseSchemaalso gained thedeltaanddeltaCursorfields already present onlistTracesResponseSchema, so lightweight lists can be live-tailed.Note that
paginationonListTracesLightResponseis now optional, because delta-mode responses return a cursor instead of a page. Code that reads it directly needs a guard:// Before const total = response.pagination.total; // After const total = response.pagination?.total ?? 0;
-
Added
WorkspaceSandbox.snapshot()for persisting sandbox state when supported. (#21221)await sandbox.snapshot();
-
Added an optional
reasonwhen declining a tool call, so the model and your UI can see why a tool call was rejected instead of always showing "Tool call was not approved by the user". (#21085)// Before: the model only learned the call was declined await agent.declineToolCall({ runId, toolCallId }); // After: give the model context it can act on await agent.declineToolCall({ runId, toolCallId, reason: 'Reading other users personal data is not allowed, ask the user for their own email instead', });
The reason is retained with the tool call, so it is still there when the conversation is recalled later. It is supported by
declineToolCall,declineToolCallGenerateanddeclineNetworkToolCall, on both regular and durable agents. Omittingreasonkeeps the previous default message.Closes #20495
-
Added an
onDetectioncallback toPIIDetectorandPromptInjectionDetectorso you can observe detection results and build your own metrics. (#21123)The callback receives the raw detection result, the analyzed input, whether it crossed the threshold, and which strategy was applied.
new PIIDetector({ model: 'openai/gpt-4o-mini', onDetection: ({ detectionResult, input, flagged, strategyApplied }) => { metrics.increment('pii.detections', { flagged, strategyApplied }); }, });
Errors thrown from the callback are logged and never interrupt processing. Closes #13336
Patch Changes
-
Stop AgentController sessions from re-sending the whole conversation state on every streamed chunk. (#21095)
A session emits one
message_updateper streamed delta, and the session bus fanned out a full
display_state_changedsnapshot alongside each one. Every snapshot carried the growing message plus
each finished tool's arguments and result, so a long turn with a large tool result re-sent that result
thousands of times. A turn with 500 deltas and a 100 KB tool result put roughly 50 MB of snapshots on
the wire; subscribers holding the event log could exhaust memory.Snapshots for high-frequency events are now coalesced, so a burst of deltas produces one snapshot per
frame instead of one per delta. A snapshot always describes the complete current state, so no
information is lost: any dropped intermediate snapshot is immediately superseded, and a pending
snapshot is flushed before the next non-coalesced event so ordering and final state are unchanged.
The engine also skips its per-delta message clone when a session has no subscribers.Event types, payload shapes, and delivery order are unchanged.
-
Fixed
AgentController.resolveWorkspacehanding one session's workspace to every session created after it. On a controller configured with a dynamic workspace factory, the first call cached its result over the factory itself, so later sessions skipped resolution and ran against the first session's workspace instead of their own. (#21144)resolveWorkspacenow returns the workspace a session already resolved at creation, and caches nothing on the controller. Two smaller fixes come with it:isWorkspaceReady()no longer flips tofalsefor factory configs, and slash commands read the same workspace instance the session's runs use. -
Update provider registry and model documentation with latest models and providers (
45a9147) -
Fixed thread title generation being dropped on serverless runtimes by accepting an optional
serverless.waitUntilongenerate()/stream()so title persistence stays alive after the response without blocking the run. (#20682) (#20996)import { waitUntil } from '@vercel/functions'; await agent.generate('Name this conversation', { serverless: { waitUntil }, memory: { thread: 'thread-1', resource: 'user-1', options: { generateTitle: true }, }, });
-
Made the workspace LSP manager's per-file lock hand out access in arrival order. Diagnostics requests for the same file were already serialized; a request that arrived at the moment the previous one finished could jump ahead of requests that had been waiting longer. Requests for different files still run in parallel. (#21101)
-
Fixed deferred notifications accumulating dead workflow records forever. The internal dispatcher runs on a schedule (every minute by default) and left a completed snapshot row behind on every run, so
mastra_workflow_snapshotgrew unboundedly — tens of thousands of rows that were never read again. These runs no longer persist snapshots. Fixes #20254 (#20970) -
Add a workflow
onStartlifecycle hook. It is awaited before a run executes, receives the run context, and fires only on initial start — not on resume, restart, or time travel. UnlikeonFinish/onError, errors thrown inonStartreject thestart()/stream()call so it can act as a pre-flight gate, for example a quota check. (#21063) -
Add
bundler.minifyto minifymastra buildoutput (#21032)mastra buildalways emitted unminified code, which is larger than necessary when packaging for production — a container image or an on-prem deployment.Set
bundler.minify: trueto minify the emitted bundle. Minification runs over whole chunks, so comments and whitespace are dropped and local identifiers are shortened while exported names are preserved.export const mastra = new Mastra({ bundler: { minify: true, }, });
Defaults to
false, so existing builds are unchanged.mastra devis never minified. -
Fixed dynamic workflows so object-form mapping configs keep working. Passing
mapConfigas an object toaddDynamicWorkflowpreviously failed at run time with"[object Object]" is not valid JSON; it now stays intact when the workflow is registered, saved, and loaded. (#21287) -
Allow the browser
viewportto be set to'window'so the page matches the real browser window instead of a fixed size. AddsresolveViewportSizeandresolveLaunchViewporthelpers plus aDEFAULT_BROWSER_VIEWPORTconstant for providers to resolve the setting consistently. (#21010) -
Prevent
streamLegacy()cleanup from hanging when an observer stream has queued events that have not been consumed. (#19921) -
Fixed
Mastra.shutdown()leaving database connections open when storage is aMastraCompositeStore. The composite had noclose()of its own, so shutdown silently skipped storage cleanup and any composed adapter (Redis, LibSQL, Postgres, ...) kept its client connected — leaving processes that wait for a graceful drain, such as test runners and Kubernetes pods handling SIGTERM, hanging until they were killed. (#20629)A composite now closes everything it was built from: the
defaultandeditorstores, plus any domain that owns its own connection. Each store is closed once even when it backs several domains, and a store that fails to close is logged and skipped so the remaining handles are still released. See #20621. -
Fixed Azure OpenAI routing to reject empty deployment names. (#21105)
-
Fixed
Mastra.startWorkers()so it no longer reads the schedules store on boot when the scheduler cannot start. Boot now skips that read if you setscheduler: { enabled: false }orworkers: false. Storage adapters that need request or tenant context no longer warn on every boot. Automatic detection of persisted agent schedules and deferred notifications is unchanged when you do not opt out. (#20982) -
Fixed the response cache serving one image's answer for a different image.
ResponseCachederives its key from the resolved prompt, but URL-valued image and file parts were serialized as{}, so two requests that differed only in which image they pointed at collided on the same cache entry. URLs now contribute their full href, and inline binary data contributes a digest of its bytes instead of being expanded one property per byte (which turned a 1 MiB image into ~11 MiB of intermediate JSON, hashed synchronously before every model call). (#20656) -
Fixed durable agent completion on remote workers so processed messages persist, thread titles generate, and prior turns can be recalled. (#20926)
-
Fixed sub-agent delegation failing when an LLM sends
maxStepsas a numeric string (e.g."10"instead of10). Delegation now accepts valid numeric strings and continues to reject invalid values such as non-integers or numbers below 3. (#20793) -
Fixed
includeinlistMessagesandlistMessagesByResourceIdso it can no longer return a message that belongs to a different resource. When you pass aresourceId, the target message and its surrounding context messages now stay inside that resource. Includes that cross threads inside the same resource keep working, so semantic recall withscope: 'resource'is unchanged. (#20984)Behaviour change in the in-memory store
The in-memory store read the context window from the thread you queried. It now reads the window from the thread that owns the target message, which is what the SQL stores already did. This only changes the result when an
includeentry names a message from another thread.The in-memory store also ignored
includeinlistMessagesByResourceId. It now returns the included messages, like@mastra/libsqland@mastra/pgdo.Fixes #20604.
-
Added a
firstMeaningfulExecAttimestamp to source-control sessions, recording when the session's agent completed its first successful sandbox command. Together withfirstMessageAtthis measures time-to-first-meaningful-exec: how long a user waits between sending their first message and the agent actually doing work in a live sandbox. The value is written once per session and is available on all session read APIs; setup commands run by the platform itself (skill loading, repo checkout) do not count. (#21211) -
Fixed agent schedules targeting stored agents being permanently deleted after a server restart. Both deletion paths are covered: the scheduler tick loop no longer counts an unhydrated stored agent as a missing target (it confirms absence against the editor before reclaiming the schedule row), and the fire path resolves stored agents through the editor before self-cleaning. Schedules are never deleted when the editor lookup fails transiently — only a confirmed miss from both the registry and the editor reclaims the row. (#19791)
-
Fix
deepEqualreturningtruefor values of mismatched shapes. ADatecompared against a plain object (e.g.deepEqual(new Date(), {})) fell through to the generic object-key comparison — and since aDatehas no own enumerable keys, it matched{}. Likewise an array compared against an object with matching index keys (e.g.deepEqual([1, 2], { '0': 1, '1': 2 })) returnedtrue. Added guards so an array only equals an array and aDateonly equals aDate; the checks apply recursively to nested values. (#21146) -
Improved A2A v0.3 remote task continuation. A2AAgent now resumes input and authentication requests using the original task ID and surfaces protocol errors returned by remote agents. (#20708)
const resumedResult = await remoteAgent.resumeGenerate({ approved: true }, { runId }); console.log(resumedResult.text);
-
Fixed DurableAgent terminal cleanup so it also clears
workflow.events.v2.<runId>, preventing orphaned no-TTL counter keys on persistent caches (#20786). (#20961) -
Fixed durable agent streams crashing when consumed by
@mastra/ai-sdkand other chunk converters. Thestep-startstream chunks now use the canonical shape, matching the regular engine and preventing destructuring errors when reading the chunk payload. (#19575) -
Fixed Workspace search indexing sending one embedding request per document. (#21067)
A batch-capable embedder (one branded with
batch: true) was only used when the search engine ran in lazy mode, whichWorkspacenever enables. Indexing a directory therefore cost one embedding round trip per file no matter what the embedder supported — indexing 500 files made 500 requests.indexManynow groups documents into batched embedder calls whenever the configured embedder is batch-capable, so those 500 files take 2 requests withmaxBatchSize: 256.Vector writes are bounded as well: a single
upsertnow carries at mostmin(maxBatchSize, 100)vectors instead of the whole batch, since each document's metadata carries its full text and several vector stores reject oversized write requests. Lazy-mode rebuilds that previously issued one largeupsertper flush now issue several bounded ones.An embedder declaring an unusable
maxBatchSize(0, negative, orNaN) now falls back to the default batch size instead of hanging or silently indexing nothing. -
Enqueue a
tool-output-deniedchunk when arequireApprovaltool is declined so live AI SDK clients resolve the pending tool call instead of hanging. Persistence asoutput-deniedalready worked; only the stream path was missing. (#20886)for await (const part of toAISdkStream(result.fullStream, { from: 'agent' })) { if (part.type === 'tool-output-denied') { // Clears the pending requireApproval tool call on the client console.log('denied', part.toolCallId); } }
-
Fixed dataset experiment runs failing with a missing-thread memory error when the target agent has memory configured and the request context provides only a resource id (for example from auth middleware or Studio). Each dataset item now runs in its own fresh memory thread; explicitly supplied thread ids are still respected. Fixes #20663 (#20844)
-
Fixed nested agent-as-tool approvals so users see the inner tool and arguments while resumes retain the parent delegation identity (#20934). (#20948)
-
Fix
tryStreamWithJsonFallbacktreating a valid falsy structured-output value as undefined. The first-attempt check used!object, so a schema resolving to a falsy-but-defined value (e.g.z.boolean()->false,z.number()->0) was wrongly rejected and triggered an unnecessary JSON-prompt fallback stream. It now checksobject === undefined, matching the generate path and the stream fallback path. (#20628) -
Fixed an issue where observer cleanup failures no longer prevent legacy workflow streams from completing cleanly. (#20652)
-
Fixed workflow
.map()step arrays so they preserve branch results of{},0,false, and empty strings instead of returningnull. (#20896) -
Fixed a crash where updating a thread without a title (for example during observational memory buffering) could write a null title and violate the database's not-null constraint when running a newer @mastra/memory against an older storage package. Memory now checks whether the connected storage adapter supports partial thread updates and backfills the existing title for older adapters, so mixed-version deployments keep working. See #21041 for the original title-clobbering fix this makes backward compatible. (#21257)
-
Fixed durable agents losing custom model-facing tool output between workflow steps. (#20176)
-
Hardened
runEvalsthreshold checks: non-finite scores (for exampleNaN) now failmin/maxrange thresholds instead of passing, and invalid threshold shapes passed from JavaScript (strings,null, arrays) are rejected with a clearINVALID_SCORER_THRESHOLDerror instead of silently passing every score. (#20145) -
Fixed concurrent workflow tool approvals so each suspended call resumes the correct workflow run. (#20347)
-
Storage adapters now declare support for partial thread updates, letting newer @mastra/memory preserve existing thread titles instead of overwriting them, while remaining safe against older versions. (#21257)
-
Fixed experiment runs reporting a failed outcome instead of cancelled when cancellation interrupted an in-flight item. Cancelled experiment runs now consistently finish with a cancelled outcome, so standalone experiment workers exit with the cancellation exit code instead of a failure code. (#20719)
-
Fixed an active goal being reported as cancelled when its objective was written while a run was already in flight. The objective a goal state projection sees is now read from storage whenever the run-start read found nothing, so a goal started or restarted mid-run is no longer projected as having no objective. (#21255)
-
Fixed subscribed agent-controller runs so dynamic workspaces use the identity from the request that started the run. (#20658)
-
Fixed sendStreamResume() to resume suspended agent runs from storage after a server restart or when requests reach another instance. (#20602)
-
Fixed an agent reply loop on the iMessage channel. iPhone read receipts arrive as inbound messages with no text and no attachments, and each agent reply triggered another receipt. Channel messages with neither text nor attachments no longer start an agent run. Custom channel handlers still receive them. (#21240)
-
Fixed
session.abort()when a tool call is waiting on approval or parked in a suspension. (#20972)Aborting from a
tool_approval_requiredsubscriber raised an error and ended the run with reasonerror. It now completes with reasonaborted, and the gated call settles asoutput-deniedinstead of rendering as still in flight.Aborting while tool suspensions were parked (
ask_user,request_access) dropped them silently, leaving prompts on screen whose answers could never be delivered. Each dropped suspension now emitstool_suspension_cancelled. Fixes #20592 -
Multi-step model generations now retain provider metadata from every completed step. (#19921)
-
Fixed a hang when two streams were started for the same workflow run at once. Resuming or time-traveling a suspended run twice concurrently (a double-clicked approval, a client retry, or two open tabs) left one of the two streams open forever, so the matching resume-stream or time-travel-stream HTTP request never ended. Each stream now closes itself independently. (#21126)
-
Agent scorers now run on the Inngest durable engine. (#21038)
An agent configured with
scorersnever had them executed when running viacreateInngestAgent()— no scorer ran, no spans, no persisted scores, and no error. Core's durable workflow gained anexecute-scorersstep that the Inngest workflow builder, a copy of it, never picked up.Scorer execution now lives in the durable workflow's shared module and is used by both engines, so scorers behave identically on either one.
-
Fixed output processors not being able to clear the final agent text. An output processor that redacts all assistant text to an empty string now correctly results in an empty
result.textfromgenerate()andstream(), instead of falling back to the original unprocessed model output. Fixes #19240 (#20998) -
Fix durable agents dropping
requestContextwhen a step is rehydrated on another process (for example an Inngest worker delegating to a subagent). Tool, memory, and workspace resolution now fall back to the run-level request context when the step input carries no request-context snapshot, so request-scoped configuration reaches subagents instead of resolving with an empty context. (#21015) -
Stamp the run's traceId into persisted assistant message metadata so a stored message can be correlated back to its trace. (#20928)
Previously a caller holding only a
messageIdhad no supported way to find the trace that produced it: message rows carry notraceIdcolumn and span records carry nomessageId. The traceId now rides along in the metadata that already carriedmodelIdandprovider, on both the regular and the durable agent path.const { messages } = await memory.recall({ threadId, perPage: false }); const traceId = messages.find(m => m.id === messageId)?.content.metadata?.traceId;
This is forward-looking — messages persisted before this change have no traceId.
-
Fixed deferred notification deliveries failing with "No model selected" when the woken thread had no request context. The notification dispatch workflow re-sends deferred and summarized notifications long after the originating send, so any stream options attached to the original signal are gone; waking an idle thread then had no way to resolve a model. The notification delivery policy now drives the fix:
NotificationDeliveryDecisionacceptsstreamOptions, and the dispatcher re-runs the agent's delivery policy at delivery time (via the newagent.resolveNotificationDeliveryDecision()) to attach freshly resolved stream options to both individual and summary deliveries. OnlystreamOptionsis honored at dispatch time; the record's persisted schedule still governs when and how it is delivered. Receipt-time sends also honor the policy'sstreamOptionsnow: an immediate deliver or summarize-now wake attaches them too, with caller-supplied stream options taking precedence. Mastra Code wires its session-based stream options resolver through the Code Agent'sdeliveryPolicy.decide, layered on the default decision logic. This fix applies to threads whose session is live in the current process; deliveries with no resolvable session fall back to a bare wake, and the "No model selected" error now distinguishes the case where a run started without any controller session context. This re-lands the capability removed by the #18637 revert in the policy-driven shape that revert called for, and the@mastra/github-signalsbump only widens itsgetNotificationStreamOptionscallback return type to allowundefined. (#21113) -
Fix DurableAgent inference crashes and error reporting (#21138): (#21224)
- DurableAgent inference no longer crashes with "Cannot read properties of undefined (reading 'type')" on malformed message content.
- Durable LLM errors now keep their original message, name, stack, and cause when reported to callers and
onError.
-
Let
onDelegationCompletecorrect a delegation result within the current run (#21042)A subagent that stops on a tool-calls step returns empty text, which the parent model
reads as a successful but empty delegation and narrates around ("I'll report back once
it returns"). Thefeedbackreturned fromonDelegationCompleteis persisted to the
parent's memory, so it only reaches the model on the next turn — after the parent has
already answered.The hook can now return
resultText, which replaces the tool result text the parent
model sees for that delegation in the run that is still executing. -
Added an
onSessionStarthook toAgentControllerChannelsconfig, called once per session after it is bound to its mapped chat thread and before the first message dispatches. Messages arriving while the hook is still running wait for it instead of dispatching on an unconfigured session. (#20832)const controller = new AgentController({ id: 'support-controller', agent, channels: { adapters: { slack: createSlackAdapter() }, onSessionStart: async ({ session, thread }) => { await session.model.switch({ modelId: 'anthropic/claude-sonnet-4-6' }); }, }, });
A host can already name a channel session through
resolveResourceId, but it never receives theSessionobject, which is created inside the channel machinery. Without a seam there, a host could only configure sessions it created itself, and channel-created sessions silently ran on the built-in default models. Hook errors are logged and swallowed so a session that cannot be configured still answers the message. -
Make
abort()stop durable agent runs that are executing in another process (#21009)A durable agent's steps often run somewhere other than the process that started them — another replica behind a load balancer, or an Inngest step worker.
abort()only flipped an in-memoryAbortController, which those processes never see, so aborting a durable or Inngest agent run silently did nothing in exactly the deployments durable agents exist for.Abort intent now travels over pubsub. The executing process picks the request up and flips its own controller, so the run unwinds the same way an in-process abort does and still emits its terminal stream event — consumers waiting on the stream are released instead of hanging.
abort()now returns a promise so callers can await dispatch; ignoring it preserves the previous fire-and-forget behavior. -
Fixed suspended runs that could fail to save after repeated tool calls. (#21002)
A run's snapshot no longer grows with every step it took before suspending, which keeps agents that make many tool calls before their first approval prompt under MongoDB's 16 MB per-document limit — past it, the snapshot was never written and the resume reported the run as missing. Runs suspended by earlier versions still load.
-
Fixed background tool calls so the model sees the completed result instead of the dispatch placeholder. (#21001)
An agent that dispatched a tool to the background kept reading "Background task started..." on every later turn, so it would re-run the tool or answer without the result. Unrelated provider metadata on the tool call is now preserved when the completed result arrives.
-
Fixed AgentController losing the second auto-approved tool result when sequential tool calls resume from a stale suspended snapshot (#19814) (#19940)
-
Fixed
convertFullStreamChunkToUIMessageStreamfrom@mastra/core/streamdropping the finish reason. The terminalfinishchunk now carriesfinishReason, so a UI message stream built on this export can tellstop,length,content-filter,tool-callsandotherapart. Fixes #20562. (#20983) -
Fixed assistant message history so provider-executed tool failures are preserved instead of remaining as pending calls. Matching tool invocations now use
state: "output-error", and the normalized provider message is stored inerrorTextwith a fallback when no usable message is available. Fixes #20715 (#20716) -
Resolve dynamic models once when preparing assigned tools. (#21281)
-
Fixed input processors receiving an undefined agent in their context when a durable agent run was resumed by a signal or schedule. The running agent is now passed through the processor workflow, so processors that rely on it (such as working memory and semantic recall) work correctly on wake. (#21102)
-
Fixed parallel sub-agent approvals and suspensions so every persisted target remains resumable after refresh. (#20700)
-
Fixed failed workspace commands to label standard output and standard error. (#20774)
-
Fixed
result.objectbeing stale after an output processor rejected a structured-output attempt withabort(reason, { retry: true }). The retried attempt object is now returned, matchingresult.text, instead of the rejected attempt object. Fixes #20570 (#20999) -
Fixed RegexFilterProcessor redact strategy to redact matches split across stream chunks, and added a
streamCarryoverSizeoption for custom rules whose matches can be longer than the default 128-char window. Fixes #21049 (#21050) -
runEvalsnow acceptsgatestogether with a categorized scorer config (AgentScorerConfig/WorkflowScorerConfig) without a TypeScript error. Previously a call likerunEvals({ target, data, gates, scorers: { trajectory: [...] } })failed to compile with TS2769 because the categorized-config overloads didn't declaregates, even though the runtime already ran gates independently of the scorer shape. The optionalgatesproperty was added to both the agent and workflow categorized-config overloads. (#21143) -
Fixed model configuration validation to safely reject null values. (#21192)
-
Fixed AgentController session preferences (thinking level, notifications) reverting to defaults after a server restart. These preferences now survive restarts and follow the conversation: reopening a thread restores the values that were active in it, including on self-hosted Factory deployments. (#20901)
-
Fixed live scorer execution across multiple Mastra instances so only the instance that emitted a scorer run handles it. (#20840)
-
Fixed two thread-stream broadcast issues when subscribing to agent threads: (#21224)
- Broadcast payload sanitization (#21219): broadcast copies of
step-start,step-finish, andfinishparts no longer embed the raw model request body or duplicated step history, preventing multi-gigabyte pubsub streams and out-of-memory crashes when threads contain large media. - Phantom replay prevention (#21223): runs that failed before persisting any messages no longer replay as phantom partial messages to new thread subscribers on retained backends like Redis Streams.
- Broadcast payload sanitization (#21219): broadcast copies of
-
Stop the browser-safe
@mastra/core/a2a/cliententry from pulling in the Nodemodulebuiltin, which broke@mastra/client-jsin browser bundles (Module not found: Can't resolve 'module'in Next.js). (#21053) -
Fixed a bug where a server-side tool that was still running when a request was aborted (for example, when a turn parked waiting on a client-side tool and the response closed, or the user hit Stop) was persisted as a completed tool call whose result was the abort message. On resume, the cancelled — and possibly half-finished — operation read as a successful tool call. (#18034)
Tool executions interrupted by an aborted request are now left as incomplete calls instead of being recorded as fabricated successful results, so they are no longer mistaken for completed work when a conversation resumes. Genuine tool errors on a live request still surface to the model as before.
-
Fixed stopping an agent-controller run so it always takes effect. When the model stream hung and never reacted to the abort signal, the session stayed stuck in a running state until the server was restarted; the run now finalizes as aborted a few seconds after the stop request. (#20835)
-
Added a dedicated
SKILL_RESOLUTIONspan type for dynamic agent skills resolvers, replacing theGENERICtype theresolve-skillsspan used before. The span now reportsagentIdandskillCountas typed span attributes. If you filter or query traces by span type, the resolver span's type value changed fromgenerictoskill_resolution. (#21232) -
Pass
formatLocationtoSkillsProcessorwhen skill files are not at${skill.path}/SKILL.mdfrom the model's point of view, such as when the agent's filesystem tools run against a sandbox that mounts them elsewhere. Key the override onskill.pathso skills that share a name still render distinct locations. (#20893)new SkillsProcessor({ workspace, formatLocation: skill => `/mnt/skills${skill.path}/SKILL.md`, });
Remapped locations remain valid skill identifiers: the processor registers each rendered location as an alias with the skills registry, so the
skillandskill_readtools resolve it back to the underlying skill. The skill-tool instruction now also tells the model thatlocationmay not exist on its filesystem, so it reads skill files withskill_readinstead of filesystem tools. If a customWorkspaceSkillsimplementation does not support alias registration, the instruction falls back to directing the model to refer to skills by name. -
Fixed dynamic
skillsresolvers running four times per agent request. Skills are needed in several places during one execution, and each of them called your resolver again — so a resolver that fetches over the network made four calls per request instead of one. (#20921)const agent = new Agent({ name: 'support', instructions: 'Help the customer.', model: 'openai/gpt-5-mini', skills: async ({ requestContext }) => { // Called four times per generate()/stream() before this change, once now. const res = await fetch(`https://internal/skills?user=${requestContext.get('userId')}`); return (await res.json()).skills; }, });
The resolution is now shared across a request, keyed on its
RequestContext. A new request resolves again, and a failed resolution is not cached so a retry still reaches your resolver. If you reuse a singleRequestContextacross several executions, those executions now share one resolution. -
Fixed Mastra shutdown so background task resources are released before storage closes without blocking indefinitely. Retryable running tasks remain recoverable after a process restart, while local executors are aborted and durable dispatches are left for surviving workers during shutdown. (#20194)
-
Fix dedicated scheduler deployments never starting the scheduler. When
MASTRA_WORKERS=scheduler(or any worker filter naming theschedulerrole) is set,startWorkers()now always injects the SchedulerWorker instead of relying on boot-time heuristics (declarative workflow schedules or persisted agent-schedule rows) that a standalone scheduler process cannot see — it exists to fire schedule rows created by other processes.workers: falseandscheduler: { enabled: false }still take precedence. (#21224) -
Messages sent while an agent run is active are no longer silently lost when the run fails. (#21128)
Failed runs now finish cleanly. A run that dies on a provider error previously never settled its completion watcher, so queued messages were never delivered, the thread stayed locked, and no error surfaced.
Queued messages survive delivery failures. If starting the follow-up run for a queued message fails, the message is put back at the head of the queue and the failure renders as an error. The message delivers on the next turn.
-
Fixed tool calls that fail during execution staying stuck as running in agent-controller session streams. A tool error now updates the streamed message part to a terminal errored result and emits the corresponding message update, exactly like a successful tool result. The
isErrorflag on persisted tool invocations is now part of the public type instead of an undeclared runtime field. (#20805) -
Fixed
@mastra/corecrashing when a project loads Mastra as CommonJS. Several ESM-only dependencies were read as a module object instead of a function, so the calls threw. (#20980)What failed before
new MCPServer(...)threwslugify is not a function.mastra.schedules.create()threw the same error.- Workspace search, workspace file indexing, and batch trace scoring threw
p_map.default is not a function.
All of these paths now run under CommonJS and ESM. See #20354.
-
Fixed observational memory stalling agent loops after a tool fails. (#20788)
-
Fixed streamed assistant messages carrying a different id than their persisted copy. Chat UIs that reconcile a live stream with refetched history could show the same assistant reply twice (for example after returning to a hidden tab); with a shared id, deduplication by message id now works for every turn, including text-only replies without tool calls. (#21185)
-
Acknowledge pubsub deliveries so persistent backends stop accumulating pending entries. Every fan-out subscribe creates a private consumer group, and Redis keeps each delivered entry pending until it is acked, so subscribers that never acknowledged grew an unbounded pending list for as long as they stayed attached. (#21081)
The following subscribers now ack every event they inspect (including ones they filter out) and nack when processing throws:
- the agent thread-stream subscriber and the cross-agent remote-run waiter, which also waits for the terminal event's ack before unsubscribing
- workflow run watchers, including the shared
nested-watchtopic - durable agent abort-request listeners
- user-defined topic listeners registered through
eventsoraddTopicListener
eventslisteners are typed as receiving only the event; acknowledgement is handled for them. -
Fix schema-based working memory losing stored data on partial updates. When a model updated one section of working memory, unrelated sections could be wiped out. Tools can now set
strict: falseto opt out of strict structured-output schema rewriting, which previously forced every field to be required and left models no way to signal "leave this field alone". (#20992) -
Fixed unbounded memory growth in the shared event cache when a caching pub/sub is backed by a persistent store such as Redis. Workflow watch events and other instance-local events were still being copied into the shared replay cache even though no other instance could read them. Because those events carry cumulative step results, a single workflow run could add tens of megabytes of cache entries that nothing ever consumed, and each topic left behind an index counter that was never cleaned up. Instance-local events are now delivered live only and skip the cache entirely; replay for agent streams is unchanged. Fixes #20646 (#20685)
-
Fixed generated thread titles being clobbered during a turn (#21041)
updateThreadrequired bothtitleandmetadata, so callers that only needed to
change metadata (message persistence, working memory, observational memory, channel
subscriptions) had to read the thread and pass its title back. When title generation
finished between that read and the write, the freshly generated title was overwritten
with the stale one.titleandmetadataare now independently optional: omitting one leaves that column
untouched. Callers that only change metadata no longer send a title, and message
persistence no longer rewrites a thread row it just read. -
Make
ProviderModelsMapaugmentable so custom gateways can register their providers and models. It is now declared as an exported interface on@mastra/core/llm, sodeclare module '@mastra/core/llm'augmentation flows through toProvider,ModelForProviderandModelRouterModelId. (#21043) -
Fixed dataset item saving for traces with failed or suspended tool calls that have no recorded results. These dataset items now save successfully; missing tool results are stored as
nullinstead of being omitted. (#20569) -
Stop timeTravel from destroying recorded workflow snapshots. Two changes: (#21222)
-
timeTravel now fails with a descriptive error and leaves the recorded snapshot unchanged when the workflow graph has changed since the run was recorded. Steps inside preceding foreach or loop entries are not inspected by this check.
-
Unnamed .map() steps now get deterministic ids, so timeTravel works across process restarts for unchanged workflow code. Removing a .map() call is not detected, so re-run rather than time travel after deleting a mapping step.
-
-
Fixed tool execute-time input validation for Zod tools on Anthropic Claude 3.5 Haiku. The compat layer now skips string min/max checks that were removed from the model-facing JSON Schema, while preserving refinements, defaults, and other validation semantics. (#19701)
-
Exported the
RunEvalsResulttype so the return type ofrunEvalscan be imported from@mastra/core/evals. This also fixes a type error introduced in #21143 where the type-level tests imported a type that was not exported. (#21196) -
Fixed workflow run streams (
WorkflowRunOutput) swallowing stream pipeline errors, which could hang callers forever. (#18571)When the underlying stream errored (for example a provider/transport failure mid-run), the error was swallowed and the run never finalized —
await output.result/output.usageand anyfullStreamconsumers waited forever. These now reject with the error, the run is markedfailed, and consumers receive a terminalworkflow-finishevent and close.const result = await run.stream(input).result; // before: hung forever on a stream error // after: rejects with the error
-
Fixed
LocalSandboxreplacing your custom seatbelt profile. When you pointnativeSandbox.seatbeltProfilePathat a profile file you wrote, that profile now stays active after a mount or an unmount. (#20978)Mastra uses your profile exactly as written and does not add mount paths to it, so the profile must already allow every path you mount. If no file exists at that path, Mastra still generates a default profile, and that generated profile keeps allowing the paths you mount. Generated profiles now carry a marker comment, so a later run regenerates them and keeps allowing mount paths instead of reading them back as your own profile. To edit a generated profile and keep your edits, delete that marker comment: the file then counts as yours.
-
Fix durable agents losing the caller's request context after the first iteration. (#21125)
prepareForDurableExecutionsnapshots the caller'sRequestContextonto the workflow input asrequestContextEntries, and steps that rebuild the model and tools from the Mastra instance restore the context from that snapshot. The snapshot was dropped when iteration state was rebuilt and was never forwarded to the LLM step, so from the second iteration on, dynamicmodel,tools,memoryandworkspaceresolvers ran against an empty context and silently fell back to defaults. This affected every path that cannot use the in-process run registry: Inngest workers, recovered runs, and evicted registry entries.The snapshot is now declared on the iteration state and LLM step input schemas, carried forward by
createBaseIterationStateUpdate, and forwarded by themap-to-llm-inputstep in both the core and Inngest durable agentic workflows. -
Added MCP server context compatibility for tools using the MCP 2.0 packages. (#18683)
-
Fixed destroyed workspaces holding on to their indexed documents and loaded skills. Long-running apps that create a workspace per session no longer grow in memory as sessions come and go. (#20617)
Also in this change
- Cleanup now runs even when another part of the workspace fails to shut down.
- Indexing or accessing skills after workspace teardown begins now throws
WorkspaceNotReadyErrorinstead of quietly repopulating released content.
@mastra/agent-browser@0.5.1
Patch Changes
- Honor a
'window'viewport where the provider can support it. Agent Browser and the browser viewer disable viewport emulation so the page tracks the real window; Stagehand does the same when connecting over CDP and falls back to the default size for a locally launched browser, which always applies its own viewport. (#21010)
@mastra/ai-sdk@1.8.0
Minor Changes
-
Added optional SSE heartbeats to
chatRoute()so streams can remain active through infrastructure with idle timeouts. (#20852)chatRoute({ path: '/chat/:agentId', heartbeatMs: 15_000, });
-
Fixed nested agent streams to emit compact snapshots until each step completes, and added
data-tool-agent-stepas a new stream part type (exported asAgentStepDataPart) that carries the full completed-step detail for consumers that observe suspension without a finish event. (#20576)
Patch Changes
-
Enqueue a
tool-output-deniedchunk when arequireApprovaltool is declined so live AI SDK clients resolve the pending tool call instead of hanging. Persistence asoutput-deniedalready worked; only the stream path was missing. (#20886)for await (const part of toAISdkStream(result.fullStream, { from: 'agent' })) { if (part.type === 'tool-output-denied') { // Clears the pending requireApproval tool call on the client console.log('denied', part.toolCallId); } }
-
Fixed the finish reason dropping out of AI SDK UI message streams. The final
finishchunk fromhandleChatStreamandtoAISdkStreamnow carriesfinishReason, so clients can tellstop,length,content-filter,tool-callsandotherapart. This matches the AI SDK behavior. Fixes #20562. (#20983)
@mastra/auth-okta@0.2.0
Minor Changes
-
Added an
audienceoption toMastraAuthOktaso bearer-token verification is no longer pinned to the OAuth client ID. (#21117)Previously the provider always required
audto equal the client ID, which is the audience of an Okta ID token. Okta access tokens carry the authorization server's audience instead, so machine-to-machine callers (MCP clients, CI jobs, service-to-service traffic) always got a 401 on an org authorization server. Those callers have no session cookie, so bearer was their only way in.Set
audience(or theOKTA_AUDIENCEenvironment variable) to the audience your tokens actually carry:// Before: only ID tokens whose aud is the client ID were accepted const auth = new MastraAuthOkta({ domain, clientId, clientSecret, redirectUri }); // After: accept access tokens from an org authorization server const auth = new MastraAuthOkta({ domain, clientId, clientSecret, redirectUri, audience: 'https://your-org.okta.com', }); // Or accept both ID tokens from the browser and access tokens from services const auth = new MastraAuthOkta({ /* ... */ audience: [clientId, 'api://default'] });
The default is unchanged, so existing setups keep working.
@mastra/browser-firecrawl@0.2.1
Patch Changes
- Honor a
'window'viewport where the provider can support it. Agent Browser and the browser viewer disable viewport emulation so the page tracks the real window; Stagehand does the same when connecting over CDP and falls back to the default size for a locally launched browser, which always applies its own viewport. (#21010)
@mastra/browser-viewer@0.2.2
Patch Changes
- Honor a
'window'viewport where the provider can support it. Agent Browser and the browser viewer disable viewport emulation so the page tracks the real window; Stagehand does the same when connecting over CDP and falls back to the default size for a locally launched browser, which always applies its own viewport. (#21010)
@mastra/clickhouse@1.15.0
Minor Changes
-
Memory list reads now surface database errors instead of silently returning empty results. (#17910)
Previously, the paginated memory reads (
listThreads,listMessages,listMessagesByResourceId, andlistMessagesById) caught backend failures, logged them, and returned an empty payload like{ threads: [], total: 0, hasMore: false }. A transient outage (locked table, dropped connection) was therefore indistinguishable from a genuinely empty result, so an agent reading conversation history during a brief failure would treat it as "no history" and could overwrite real state. These methods now re-throw the failure as aMastraError. Validation (USER) errors and genuinely empty results are unchanged.Behavior change
Callers that previously received an empty result on a backend failure will now receive a thrown
MastraError. If you call these read methods directly (rather than through an agent, which already surfaces errors), wrap them so a transient outage doesn't crash the caller:try { const { threads } = await storage.listThreads({ resourceId }); // ...use threads } catch (error) { // a real backend failure. Decide whether to retry, surface, or degrade. // An empty thread list no longer hides here; it only means "no threads". }
Patch Changes
-
Fixed a crash where updating a thread without a title (for example during observational memory buffering) could write a null title and violate the database's not-null constraint when running a newer @mastra/memory against an older storage package. Memory now checks whether the connected storage adapter supports partial thread updates and backfills the existing title for older adapters, so mixed-version deployments keep working. See #21041 for the original title-clobbering fix this makes backward compatible. (#21257)
-
Storage adapters now declare support for partial thread updates, letting newer @mastra/memory preserve existing thread titles instead of overwriting them, while remaining safe against older versions. (#21257)
-
Fixed resource-scoped message includes across storage adapters so included context cannot cross resource boundaries. (#20984)
-
Fixed generated thread titles being clobbered during a turn (#21041)
updateThreadrequired bothtitleandmetadata, so callers that only needed to
change metadata (message persistence, working memory, observational memory, channel
subscriptions) had to read the thread and pass its title back. When title generation
finished between that read and the write, the freshly generated title was overwritten
with the stale one.titleandmetadataare now independently optional: omitting one leaves that column
untouched. Callers that only change metadata no longer send a title, and message
persistence no longer rewrites a thread row it just read. -
Fixed the ClickHouse lightweight trace list: responses no longer carry the
input,outputandattributespayload blobs, and delta polling now works. (#20677)listTracesLightrows now carry a shortinputPreviewin place of the full input, plus the spanmetadataand a computedstatus, so Studio's configurable trace columns keep working — in both page and delta mode. Response payloads stay small as prompts grow. There is no schema change and no migration.Requires
@mastra/core>= 1.57.0, which ships the sharedbuildInputPreviewandcomputeTraceStatushelpers this store now imports (peer dependency bumped accordingly).
@mastra/client-js@1.39.0
Minor Changes
-
Added experiment provenance and grouping support to the JavaScript client. (#20645)
await client.triggerDatasetExperiment({ datasetId, targetType: 'agent', targetId: 'agent-1', grouping: { experimentSetId: 'benchmark-1', trialIndex: 0 }, });
-
Add a reasoning-effort configuration surface across mastracode and Factory (fixes #20766): (#20884)
- New
maxthinking level (mapped toreasoning effort: maxfor OpenAI Codex and Anthropiceffort). - Anthropic extended-thinking wiring: the session thinking level now applies to anthropic/claude-opus-4-7 and other Anthropic models via provider thinking/effort options (previously OpenAI-only).
- New
models.modeThinkingDefaultssetting: per-mode (build/plan/fast) default thinking levels, resolved at request time with precedence session override → mode default → globalpreferences.thinkingLevel. Configuration changes now apply to the next request of every session, including automated Factory runs. - Factory: new Settings → Defaults controls for editing global and per-mode thinking defaults in local deployments.
- TUI:
/thinknow sets a session-only override, supports/think defaultto clear it, and/think statusreports the effective level with provenance (session override / mode default / global default).
Example
settings.jsonconfiguration:{ "preferences": { "thinkingLevel": "medium" }, "models": { "modeThinkingDefaults": { "build": "high", "plan": "max", "fast": "off" } } } - New
-
Added
getA2AV1()for opt-in A2A Protocol v1.0 requests, including task listing and v1 streaming responses. ExistinggetA2A()integrations remain on v0.3. (#20811)const a2a = client.getA2AV1('weather-agent'); const tasks = await a2a.listTasks(ListTasksRequest.fromJSON({ pageSize: 20 }));
-
Renamed the stored workflow client methods to dynamic workflows:
upsertStoredWorkflow(),listStoredWorkflows(), andgetStoredWorkflow()are nowupsertDynamicWorkflow(),listDynamicWorkflows(), andgetDynamicWorkflow(). (#20938) -
Added
isKnownAgentControllerEventto narrow agent controller stream events.AgentControllerEventincludes a forward-compatibility arm whosetypeisstring, so comparingevent.typeto a literal never narrows the union and payload fields stayunknown— consumers had to cast. (#20800)Before
session.subscribe({ onEvent: event => { const known = event as KnownAgentControllerEvent; if (known.type === 'message_end') save(known.message); }, });
After
import { isKnownAgentControllerEvent } from '@mastra/client-js'; session.subscribe({ onEvent: event => { if (isKnownAgentControllerEvent(event) && event.type === 'message_end') save(event.message); }, });
The guard is kept in sync with the event union at compile time, so a new event type cannot be added without it.
-
Added
listWorkflowRunCounts()— fetches per-workflow counts of running and suspended runs from the new aggregated server endpoint in a single request. (#18925)const runCounts = await mastraClient.listWorkflowRunCounts(); // { "cityWorkflow": { running: 2, suspended: 1 }, ... }
Servers that predate the endpoint respond with
404 Not Found. -
Added
listTracesLight()for fetching trace lists without theinput,outputandattributesblobs. It takes the same filtering, ordering and delta-polling arguments aslistTraces(), and each row carries a shortinputPreviewinstead of the full input. (#20677)// Full payloads — use when you need attributes/input/output const full = await client.listTraces({ pagination: { page: 0, perPage: 25 } }); // Lightweight rows for list views; fetch the full record when a row is opened const list = await client.listTracesLight({ pagination: { page: 0, perPage: 25 } }); list.spans[0].inputPreview; // 'summarize this thread'
Patch Changes
-
Preserve experiment name, description, and metadata from HTTP trigger requests. (#20578)
-
Added typed display-state fields to the agent-controller
display_state_changedevent:activeTools,toolInputBuffers,pendingSuspensions,activeSubagents, andmodifiedFilesare now declared on the event payload. (#20805)const session = client.getAgentController('code').session('user-1'); await session.subscribe({ onEvent: event => { if (event.type === 'display_state_changed') { for (const [toolCallId, tool] of Object.entries(event.displayState.activeTools ?? {})) { console.log(`${tool.name} is ${tool.status} (${toolCallId})`); } } }, });
-
Added the
skill_resolutionspan type to the generated route types. (#21232) -
Added an optional
reasonwhen declining a tool call, so the model and your UI can see why a tool call was rejected instead of always showing "Tool call was not approved by the user". (#21085)// Before: the model only learned the call was declined await agent.declineToolCall({ runId, toolCallId }); // After: give the model context it can act on await agent.declineToolCall({ runId, toolCallId, reason: 'Reading other users personal data is not allowed, ask the user for their own email instead', });
The reason is retained with the tool call, so it is still there when the conversation is recalled later. It is supported by
declineToolCall,declineToolCallGenerateanddeclineNetworkToolCall, on both regular and durable agents. Omittingreasonkeeps the previous default message.Closes #20495
@mastra/cloudflare@1.6.2
Patch Changes
-
Fixed a crash where updating a thread without a title (for example during observational memory buffering) could write a null title and violate the database's not-null constraint when running a newer @mastra/memory against an older storage package. Memory now checks whether the connected storage adapter supports partial thread updates and backfills the existing title for older adapters, so mixed-version deployments keep working. See #21041 for the original title-clobbering fix this makes backward compatible. (#21257)
-
Storage adapters now declare support for partial thread updates, letting newer @mastra/memory preserve existing thread titles instead of overwriting them, while remaining safe against older versions. (#21257)
-
Fixed resource-scoped message includes across storage adapters so included context cannot cross resource boundaries. (#20984)
-
Fixed generated thread titles being clobbered during a turn (#21041)
updateThreadrequired bothtitleandmetadata, so callers that only needed to
change metadata (message persistence, working memory, observational memory, channel
subscriptions) had to read the thread and pass its title back. When title generation
finished between that read and the write, the freshly generated title was overwritten
with the stale one.titleandmetadataare now independently optional: omitting one leaves that column
untouched. Callers that only change metadata no longer send a title, and message
persistence no longer rewrites a thread row it just read.
@mastra/cloudflare-d1@1.3.0
Minor Changes
-
Memory list reads now surface database errors instead of silently returning empty results. (#17910)
Previously, the paginated memory reads (
listThreads,listMessages,listMessagesByResourceId, andlistMessagesById) caught backend failures, logged them, and returned an empty payload like{ threads: [], total: 0, hasMore: false }. A transient outage (locked table, dropped connection) was therefore indistinguishable from a genuinely empty result, so an agent reading conversation history during a brief failure would treat it as "no history" and could overwrite real state. These methods now re-throw the failure as aMastraError. Validation (USER) errors and genuinely empty results are unchanged.Behavior change
Callers that previously received an empty result on a backend failure will now receive a thrown
MastraError. If you call these read methods directly (rather than through an agent, which already surfaces errors), wrap them so a transient outage doesn't crash the caller:try { const { threads } = await storage.listThreads({ resourceId }); // ...use threads } catch (error) { // a real backend failure. Decide whether to retry, surface, or degrade. // An empty thread list no longer hides here; it only means "no threads". }
Patch Changes
-
Fixed a crash where updating a thread without a title (for example during observational memory buffering) could write a null title and violate the database's not-null constraint when running a newer @mastra/memory against an older storage package. Memory now checks whether the connected storage adapter supports partial thread updates and backfills the existing title for older adapters, so mixed-version deployments keep working. See #21041 for the original title-clobbering fix this makes backward compatible. (#21257)
-
Storage adapters now declare support for partial thread updates, letting newer @mastra/memory preserve existing thread titles instead of overwriting them, while remaining safe against older versions. (#21257)
-
Fixed resource-scoped message includes across storage adapters so included context cannot cross resource boundaries. (#20984)
-
Fixed generated thread titles being clobbered during a turn (#21041)
updateThreadrequired bothtitleandmetadata, so callers that only needed to
change metadata (message persistence, working memory, observational memory, channel
subscriptions) had to read the thread and pass its title back. When title generation
finished between that read and the write, the freshly generated title was overwritten
with the stale one.titleandmetadataare now independently optional: omitting one leaves that column
untouched. Callers that only change metadata no longer send a title, and message
persistence no longer rewrites a thread row it just read.
@mastra/code-sdk@1.2.0
Minor Changes
-
Added Dynamic Workflow creation and management to Mastra Code, including discovery-backed authoring, immediate persistence, execution, and deletion. (#21210)
import { listWorkflows, runWorkflow } from '@mastra/code-sdk/workflows/service'; const { workflows } = await listWorkflows(mastra); const workflow = workflows[0]; if (workflow) { await runWorkflow(mastra, workflow.id, { topic: 'dynamic workflows' }); }
-
Added
processorsandsignalProvidersto the Mastra Code plugin contract, so a plugin can contribute more than tools. (#20848)Processors
A plugin can now extend the agent pipeline directly, passing a bare array for input processors or an object for both lanes. Plugin processors run after the processors Mastra Code configures and before the channel and memory layers the Agent appends. The slot isn't configurable, and the processors are resolved before every LLM call. Enabling, disabling, or updating a plugin applies on the next request instead of requiring a restart.
Signal providers
A plugin can also ship a signal provider, which monitors an external source and pushes notifications into a thread. Providers are long-lived, so the SDK owns their lifecycle instead of handing them to the agent: it registers Mastra on them, connects them to the coding agent, starts them polling, and stops them when the plugin is updated, disabled, or uninstalled. That makes a provider installed from a GitHub repository survive a mid-session update of that repository. Only one provider with a given id runs at a time, and a provider that fails to start is isolated from the rest of its plugin.
Field resolvers also receive
getController()andgetActiveSession()on the plugin context, so a plugin can read the running session lazily at the moment it needs it.For embedders that inject their own
pluginManagerintocreateMastraCode: thePluginManagercontract now also requiresonReload,getPluginSignalProviders, andsetRuntime, so hand-built manager implementations must add these methods. -
Add a reasoning-effort configuration surface across mastracode and Factory (fixes #20766): (#20884)
- New
maxthinking level (mapped toreasoning effort: maxfor OpenAI Codex and Anthropiceffort). - Anthropic extended-thinking wiring: the session thinking level now applies to anthropic/claude-opus-4-7 and other Anthropic models via provider thinking/effort options (previously OpenAI-only).
- New
models.modeThinkingDefaultssetting: per-mode (build/plan/fast) default thinking levels, resolved at request time with precedence session override → mode default → globalpreferences.thinkingLevel. Configuration changes now apply to the next request of every session, including automated Factory runs. - Factory: new Settings → Defaults controls for editing global and per-mode thinking defaults in local deployments.
- TUI:
/thinknow sets a session-only override, supports/think defaultto clear it, and/think statusreports the effective level with provenance (session override / mode default / global default).
Example
settings.jsonconfiguration:{ "preferences": { "thinkingLevel": "medium" }, "models": { "modeThinkingDefaults": { "build": "high", "plan": "max", "fast": "off" } } } - New
-
Added MCP disable-state controls to the MCP manager. Servers can be disabled for the current project or for every project, the state persists across runs in an app-data
mcp-state.json(user MCP config files are never mutated), and disabled servers stay visible in statuses via the newdisabled/disabledScopefields onMcpServerStatus. (#20834)await mcpManager.setServerDisabled('filesystem', true); // project scope await mcpManager.setServerDisabled('filesystem', true, { global: true }); // all projects await mcpManager.setAllDisabled(true, { global: true }); // global kill switch mcpManager.isAllDisabledGlobally(); mcpManager.getDisabledServers();
Patch Changes
-
Persist the browser viewport as a preset name, a
{ width, height }size, or'window'in settings, and drop unusable stored values back to the default rather than passing them to the browser. (#21010) -
Fixed tenant credential resolution for session-based authentication providers. Background Factory runs now resolve the authenticated user and active organization from session-wrapped request context values instead of falling back to an empty credential store. (#21008)
-
Added a
modeloption to Stagehand browser settings, so browser automation can run on a chosen provider instead of a fixed default: (#20993)import { createBrowserFromSettings } from '@mastra/code-sdk/onboarding/settings'; const browser = await createBrowserFromSettings({ enabled: true, provider: 'stagehand', headless: true, stagehand: { env: 'LOCAL', model: 'anthropic/claude-sonnet-4-5' }, });
The model must be provider-qualified as
<provider>/<model>. Values Stagehand cannot resolve, such as a baregpt-4.1, are ignored so the browser still starts. -
Fixed woken notifications running against the controller-level workspace, which is
undefinedfor dynamic workspace factories. They now use the workspace of the session that owns the target thread. (#21144) -
Fixed deferred notification deliveries failing with "No model selected" when the woken thread had no request context. The notification dispatch workflow re-sends deferred and summarized notifications long after the originating send, so any stream options attached to the original signal are gone; waking an idle thread then had no way to resolve a model. The notification delivery policy now drives the fix:
NotificationDeliveryDecisionacceptsstreamOptions, and the dispatcher re-runs the agent's delivery policy at delivery time (via the newagent.resolveNotificationDeliveryDecision()) to attach freshly resolved stream options to both individual and summary deliveries. OnlystreamOptionsis honored at dispatch time; the record's persisted schedule still governs when and how it is delivered. Receipt-time sends also honor the policy'sstreamOptionsnow: an immediate deliver or summarize-now wake attaches them too, with caller-supplied stream options taking precedence. Mastra Code wires its session-based stream options resolver through the Code Agent'sdeliveryPolicy.decide, layered on the default decision logic. This fix applies to threads whose session is live in the current process; deliveries with no resolvable session fall back to a bare wake, and the "No model selected" error now distinguishes the case where a run started without any controller session context. This re-lands the capability removed by the #18637 revert in the policy-driven shape that revert called for, and the@mastra/github-signalsbump only widens itsgetNotificationStreamOptionscallback return type to allowundefined. (#21113) -
Fixed custom provider models saved with a stray
mastracode/prefix in settings, which broke selecting and using them after choosing them from/models(#20799) (#20804) -
Fixed authentication failures for unprefixed direct-provider models by using provider environment credentials when no provider-specific stored credential exists. (#21197)
-
Fixed Factory interactive plans so they are stored as browsable artifacts. (#21173)
-
Include worktree identity (
worktree_path,branch,main_repo_path) inSessionStartandSessionEndhook payloads when a session runs in a git worktree, so hooks can provision and tear down per-worktree resources. (#21034)
@mastra/convex@1.5.3
Patch Changes
-
Fixed a crash where updating a thread without a title (for example during observational memory buffering) could write a null title and violate the database's not-null constraint when running a newer @mastra/memory against an older storage package. Memory now checks whether the connected storage adapter supports partial thread updates and backfills the existing title for older adapters, so mixed-version deployments keep working. See #21041 for the original title-clobbering fix this makes backward compatible. (#21257)
-
Storage adapters now declare support for partial thread updates, letting newer @mastra/memory preserve existing thread titles instead of overwriting them, while remaining safe against older versions. (#21257)
-
Fixed resource-scoped message includes across storage adapters so included context cannot cross resource boundaries. (#20984)
-
Fixed generated thread titles being clobbered during a turn (#21041)
updateThreadrequired bothtitleandmetadata, so callers that only needed to
change metadata (message persistence, working memory, observational memory, channel
subscriptions) had to read the thread and pass its title back. When title generation
finished between that read and the write, the freshly generated title was overwritten
with the stale one.titleandmetadataare now independently optional: omitting one leaves that column
untouched. Callers that only change metadata no longer send a title, and message
persistence no longer rewrites a thread row it just read.
@mastra/daytona@0.7.0
Minor Changes
-
Added
domainAllowListtoDaytonaSandboxOptions, for allowing outbound access to services whose IP addresses change, such as package registries and hosted APIs. CIDR-basednetworkAllowListcannot express these reliably. (#21000)const sandbox = new DaytonaSandbox({ networkBlockAll: true, domainAllowList: 'registry.npmjs.org,*.githubusercontent.com', });
The option is applied at sandbox creation and preserved by
clone(). Requires@daytonaio/sdk0.201.0 or later, which the package now depends on.
@mastra/deployer@1.58.0
Minor Changes
-
Added discovery and bundling for
instructions.tsin file-based agent directories, so an agent can define its prompt in TypeScript instead ofinstructions.md. (#20847)export default 'You are a helpful weather assistant.';
Unlike
instructions.md, whose text is inlined into the generated code,instructions.tsis imported. It can therefore import from the rest of your project, andmastra devpicks up edits through the normal module graph.A directory holding only an
instructions.tsnow counts as an agent, and subagent directories follow the same rule. Symlinkedinstructions.tsfiles are skipped, matching howconfig.tsandmemory.tsare handled.If you already keep an unrelated
instructions.tsinside an agent directory, for example a helper thatconfig.tsimports, rename it. Mastra now reads that file as the agent's instructions and the build fails if it has no default export. -
Added file-based schedules for agents (#20711)
File-based agents can now declare recurring tasks in a
schedules/directory next to their tools and skills. Mastra registers them into schedule storage at startup, so a scheduled agent no longer needs any runtime registration code.Each file is one schedule: a cron expression plus exactly one execution mode. Prompt mode runs the owning agent with a fixed message.
// src/mastra/agents/support/schedules/heartbeat.ts import { defineSchedule } from '@mastra/core/agent'; export default defineSchedule({ cron: '*/5 * * * *', prompt: 'Check system health and report any failures.', });
Handler mode computes the fire when it triggers, and returning
nullskips it.// src/mastra/agents/support/schedules/billing/sweep.ts import { defineSchedule } from '@mastra/core/agent'; export default defineSchedule({ cron: '0 3 * * *', handler: async () => { const overdue = await findOverdueInvoices(); if (overdue.length === 0) return null; return { prompt: `Chase ${overdue.length} overdue invoices.` }; }, });
A schedule's id is its path under
schedules/with the extension stripped, sobilling/sweep.tsbecomesbilling/sweepand stays stable across builds. Editing a cron patches the stored schedule and recomputes its next fire time, deleting the file deletes the schedule, and pausing a schedule through the API survives a redeploy. Schedules created withmastra.schedules.create(...)are in a separate namespace and are never touched by this sync.A schedule can also be a Markdown file, using cron frontmatter with the document body as the prompt.
// src/mastra/agents/support/schedules/cleanup.md --- cron: "0 3 * * *" --- Review tickets untouched for 30 days and close the ones that are resolved.Declaring a schedule is enough to start the scheduler. Schedules are supported on root agents only; a
schedules/directory undersubagents/fails the build, because the scheduler cannot resolve a subagent as a run target.defineScheduleis exported from both@mastra/core/agentand@mastra/core/schedules, so authoring a file-based agent needs a single import path.Build and dev support the same convention: schedules are discovered at build time, Markdown schedules fail the build with a message naming the file when the cron is missing, the body is empty, the frontmatter has an unknown field, or the YAML is unparseable. That last case has its own message because a leading
*is a YAML alias, socron: */5 * * * *needs quoting. The dev server rebuilds when a Markdown schedule changes.
Patch Changes
-
Add
bundler.minifyto minifymastra buildoutput (#21032)mastra buildalways emitted unminified code, which is larger than necessary when packaging for production — a container image or an on-prem deployment.Set
bundler.minify: trueto minify the emitted bundle. Minification runs over whole chunks, so comments and whitespace are dropped and local identifiers are shortened while exported names are preserved.export const mastra = new Mastra({ bundler: { minify: true, }, });
Defaults to
false, so existing builds are unchanged.mastra devis never minified. -
Fixed
mastra buildso the generated output keeps the dependency version ranges declared in your app'spackage.json, instead of pinning whatever version happened to be installed. An app that depends onzod: ^4.3.6next to a hoistedzod@3.25.76now gets^4.3.6in.mastra/output/package.json, so the isolated install resolves the version the app asked for. A package pinned throughoverrides,resolutions,pnpm.overridesorpnpm-workspace.yamlkeeps its resolved version, since that version was chosen deliberately. Specifiers the output directory cannot resolve, such ascatalog:,workspace:,file:,link:and git URLs, keep using the resolved version too. (#17915) -
Fixed deployment artifact generation to reject invalid pnpm build approvals and preserve configured native dependencies without loading their binaries during validation. (#20719)
-
Fixed bundler validation failing when a workspace package transitively imports a third-party dependency that was already listed in
bundler.externals. The validation subprocess now stubs user-configured externals, matching the bundler's own treatment of them. (#16639) -
Fixed user-registered middleware (
serverMiddlewareandserver.middleware) being able to return a 401 for framework-public routes such as the Studio sign-in endpoints. (#20989)The deployer now wraps every user middleware with
skipIfFrameworkPublicfrom@mastra/hono, so requests to routes declared public viacreatePublicRoute()/requiresAuth: falsealways reach their handler. -
Fixed browser A2A v1 requests by allowing the
A2A-Versionheader in the default CORS configuration. (#20811)
@mastra/deployer-sandbox@0.3.0
Minor Changes
-
Added
attachWorkerDeployment()so restarted supervisors can reconstruct worker handles from persisted sandbox and execution identities. (#21271)const worker = await attachWorkerDeployment({ sandbox, executionId }); const status = await worker.status(); const output = await worker.readOutput('stdout', { offset });
-
Added fail-closed hard resource limits for sandbox workers. (#21273)
Workers can now opt into per-attempt CPU time, address-space, file-size, and open-file limits:
await deployWorkerToSandbox({ // ... resourceLimits: { cpuTimeSeconds: 30, addressSpaceBytes: 536_870_912, fileSizeBytes: 10_485_760, openFiles: 256, }, });
Requested limits are capability-checked before deployment. CPU and file-size signal exhaustion is reported through the typed
resource_exhaustedstatus.
@mastra/dsql@1.3.0
Minor Changes
-
Memory list reads now surface database errors instead of silently returning empty results. (#17910)
Previously, the paginated memory reads (
listThreads,listMessages,listMessagesByResourceId, andlistMessagesById) caught backend failures, logged them, and returned an empty payload like{ threads: [], total: 0, hasMore: false }. A transient outage (locked table, dropped connection) was therefore indistinguishable from a genuinely empty result, so an agent reading conversation history during a brief failure would treat it as "no history" and could overwrite real state. These methods now re-throw the failure as aMastraError. Validation (USER) errors and genuinely empty results are unchanged.Behavior change
Callers that previously received an empty result on a backend failure will now receive a thrown
MastraError. If you call these read methods directly (rather than through an agent, which already surfaces errors), wrap them so a transient outage doesn't crash the caller:try { const { threads } = await storage.listThreads({ resourceId }); // ...use threads } catch (error) { // a real backend failure. Decide whether to retry, surface, or degrade. // An empty thread list no longer hides here; it only means "no threads". }
Patch Changes
-
Fixed a crash where updating a thread without a title (for example during observational memory buffering) could write a null title and violate the database's not-null constraint when running a newer @mastra/memory against an older storage package. Memory now checks whether the connected storage adapter supports partial thread updates and backfills the existing title for older adapters, so mixed-version deployments keep working. See #21041 for the original title-clobbering fix this makes backward compatible. (#21257)
-
Storage adapters now declare support for partial thread updates, letting newer @mastra/memory preserve existing thread titles instead of overwriting them, while remaining safe against older versions. (#21257)
-
Fixed resource-scoped message includes across storage adapters so included context cannot cross resource boundaries. (#20984)
-
Fixed generated thread titles being clobbered during a turn (#21041)
updateThreadrequired bothtitleandmetadata, so callers that only needed to
change metadata (message persistence, working memory, observational memory, channel
subscriptions) had to read the thread and pass its title back. When title generation
finished between that read and the write, the freshly generated title was overwritten
with the stale one.titleandmetadataare now independently optional: omitting one leaves that column
untouched. Callers that only change metadata no longer send a title, and message
persistence no longer rewrites a thread row it just read. -
Fixed transaction completion when applications start several database operations at the same time. Pending operations now finish before the transaction completes or is cancelled, preventing query conflicts after batch failures and operations that application code does not await. (#20869)
@mastra/duckdb@1.6.1
Patch Changes
-
Fixed the lightweight trace list on DuckDB ignoring delta polling and leaving the input preview column blank. (#20677)
listTracesLightpreviously ignoredmode,afterandlimit, so a client live-tailing a lightweight list refetched the first page on every poll and never receiveddeltaordeltaCursor. Delta requests now return only the traces recorded since the cursor, as lightweight rows.Rows now carry a short
inputPreviewin place of the full input, plus a computedstatusand the spanmetadata, so Studio's configurable trace columns work on the lightweight list. Page responses include adeltaCursorso polling can switch to delta mode.Requires
@mastra/core>= 1.57.0, which ships the sharedbuildInputPreviewandcomputeTraceStatushelpers this store now imports (peer dependency bumped accordingly).
@mastra/dynamodb@1.3.0
Minor Changes
-
Memory list reads now surface database errors instead of silently returning empty results. (#17910)
Previously, the paginated memory reads (
listThreads,listMessages,listMessagesByResourceId, andlistMessagesById) caught backend failures, logged them, and returned an empty payload like{ threads: [], total: 0, hasMore: false }. A transient outage (locked table, dropped connection) was therefore indistinguishable from a genuinely empty result, so an agent reading conversation history during a brief failure would treat it as "no history" and could overwrite real state. These methods now re-throw the failure as aMastraError. Validation (USER) errors and genuinely empty results are unchanged.Behavior change
Callers that previously received an empty result on a backend failure will now receive a thrown
MastraError. If you call these read methods directly (rather than through an agent, which already surfaces errors), wrap them so a transient outage doesn't crash the caller:try { const { threads } = await storage.listThreads({ resourceId }); // ...use threads } catch (error) { // a real backend failure. Decide whether to retry, surface, or degrade. // An empty thread list no longer hides here; it only means "no threads". }
Patch Changes
-
Fixed a crash where updating a thread without a title (for example during observational memory buffering) could write a null title and violate the database's not-null constraint when running a newer @mastra/memory against an older storage package. Memory now checks whether the connected storage adapter supports partial thread updates and backfills the existing title for older adapters, so mixed-version deployments keep working. See #21041 for the original title-clobbering fix this makes backward compatible. (#21257)
-
Storage adapters now declare support for partial thread updates, letting newer @mastra/memory preserve existing thread titles instead of overwriting them, while remaining safe against older versions. (#21257)
-
Fixed resource-scoped message includes across storage adapters so included context cannot cross resource boundaries. (#20984)
-
Fixed generated thread titles being clobbered during a turn (#21041)
updateThreadrequired bothtitleandmetadata, so callers that only needed to
change metadata (message persistence, working memory, observational memory, channel
subscriptions) had to read the thread and pass its title back. When title generation
finished between that read and the write, the freshly generated title was overwritten
with the stale one.titleandmetadataare now independently optional: omitting one leaves that column
untouched. Callers that only change metadata no longer send a title, and message
persistence no longer rewrites a thread row it just read.
@mastra/e2b@0.8.1
Patch Changes
- Fixed E2B command failures to avoid duplicate output and retain the terminal error. (#20774)
@mastra/editor@0.13.12
Patch Changes
- Fixed caller-scoped Composio connection management in stored agents. (#21004)
@mastra/express@1.5.0
Minor Changes
-
Added support for
createRoute()routes configured throughserver.apiRoutes. (#21184)const route = createRoute({ method: 'POST', path: '/items', responseType: 'json', bodySchema: z.object({ name: z.string() }), handler: async ({ name }) => ({ name }), }); const mastra = new Mastra({ server: { apiRoutes: [route] }, });
Patch Changes
-
Guard
reader.cancel()in the server adapters so a client disconnect cannot crash the process. (#20756)When a client disconnects mid-stream, each adapter's abort/error handler tore down the reader with an unguarded
void reader.cancel(reason). If the underlying stream's teardown rejects — for example an in-flight storage write failing while the stream is cancelled — the rejection was never handled. On Node >= 15 an unhandled promise rejection terminates the process, so a single ill-timed disconnect could take down the server and drop every other in-flight request.Cancellation is best-effort teardown, so the rejection is now swallowed with the
.catch(() => {})idiom already used elsewhere in the codebase (for exampleclient-sdks/client-jsandintegrations/livekit). Thehonoadapter already had this guard; this bringsexpress,fastifyandkoain line.
@mastra/factory@0.6.0
Minor Changes
-
Added creator and recent worker attribution to Factory board cards, with names and profile images from GitHub and Linear. GitHub pull request cards now show the author and draft, open, closed, or merged status. (#20822)
-
Added a
firstMeaningfulExecAttimestamp to source-control sessions, recording when the session's agent completed its first successful sandbox command. Together withfirstMessageAtthis measures time-to-first-meaningful-exec: how long a user waits between sending their first message and the agent actually doing work in a live sandbox. The value is written once per session and is available on all session read APIs; setup commands run by the platform itself (skill loading, repo checkout) do not count. (#21211) -
Fixed the Factory metrics so the same date range always reports the same numbers, and dropped the response fields that nothing displayed. (#21256)
Completions are events, not the board's current state. Throughput and lead time now count entries into
donein the stage history, so reopening a card no longer erases the day it shipped and a card that shipped twice counts twice. The per-day rate divides by the days the board actually existed, so a 12-month range on a two-week-old board no longer reads as ~0 per day.Automation numbers stop counting the wrong things. A card landing on the board when it is created is no longer counted as an automated stage move, which used to credit every webhook-synced card. Automation coverage measures the first pass through each stage only — a redo used to add a second entry to the denominator alone, capping a fully automated stage at 50% — and each pass's outcome is now frozen at the end of the window instead of reflecting where the card sits today.
Response shape.
stageDurations,wip,agingWipandearliestItemAtare gone: nothing rendered them, and live in-flight work is already covered by the queue-health chart.windowDaysis nowdaysCovered(the window clipped to the board's life) andcycleTimeisleadTime, which is what it always measured — card creation through todone.The metrics endpoint (
GET /web/factory/projects/:id/metrics) renames two fields:A corrupt stage-history timestamp now throws instead of being read as 1970.
-
Added stable identities and display titles for Factory user sessions. (#20781)
POST /web/github/projects/:id/sessionsnow accepts optionalsessionIdandtitlefields. Whenbranchis omitted, the session usesuser/session-<sessionId>. Callers can create a client-side draft, safely retry the first server request with the same UUID, and show the first prompt as a human-readable title. IfsessionIdis omitted, the server generates one. Explicit branches still work unchanged.const sessionId = crypto.randomUUID(); const response = await fetch(`/web/github/projects/${projectRepositoryId}/sessions`, { method: 'POST', body: JSON.stringify({ sessionId, title: 'Fix the login flow' }), });
Titles collapse whitespace, trim surrounding space, and are limited to 80 characters. Blank titles are stored as
null. -
Add a reasoning-effort configuration surface across mastracode and Factory (fixes #20766): (#20884)
- New
maxthinking level (mapped toreasoning effort: maxfor OpenAI Codex and Anthropiceffort). - Anthropic extended-thinking wiring: the session thinking level now applies to anthropic/claude-opus-4-7 and other Anthropic models via provider thinking/effort options (previously OpenAI-only).
- New
models.modeThinkingDefaultssetting: per-
- New