Highlights
Live channel resolvers (no-redeploy channel connections)
Mastra({ channels }) now accepts a resolver function (via channels() in @mastra/connect) so channel connections added/removed on Mastra Platform appear on a running server without redeploy; use mastra.resolveChannels() to fetch the current provider map at runtime.
Multi-worker safety for background tasks via persisted ownership leases
Background task execution is now lease-fenced (persisted ownerId + expiring lease) so multiple managers can share one storage without double-running tasks or allowing stale workers to overwrite results; storage adapters were updated to enforce expectedOwnerId/lease write conditions.
Stronger durability & recovery for agents/workflows (including evented engine fixes)
Durable and evented agents/workflows get major correctness upgrades: safer crash recovery, preserved timeouts across serialization, proper tool/result processing & transcript transforms on durable runs, savePerStep honored, and EventedAgent is back on the built-in evented engine with recovery support.
Thread streaming and PubSub retention improvements (faster reconnects, less replay)
subscribeToThread({ withInitialHistory }) now emits a thread-history chunk first and avoids replaying completed runs on retained backends; plus PubSub.trimTopic() (implemented in @mastra/redis-streams) lets Mastra delete finished run entries to prevent retained thread topics from growing unbounded.
Observability upgrades: trace aggregation + richer filtering
Storage adapters can now support aggregateTraces() (trace-aggregate capability) and @mastra/server adds POST /observability/traces/aggregate for grouped/time-bucketed measures; advanced trace queries also add runId/sessionId/userId/organizationId filters across supported stores.
New Microsoft Teams channel + expanded Connect integrations
Introduced @mastra/teams (Microsoft Teams channel provider) and added 25 Microsoft Teams tools in @mastra/connect; @mastra/connect also gains multi-connection support (connection selection via connection_name) and now bundles Slack/Telegram/Discord channel dependencies.
Breaking Changes
session.respondToToolApproval(...)now requirestoolCallId(id-less approval responses are rejected); related approval APIs now key gates per thread/run and update grant scoping.@mastra/playground-uiremoved/renamed multiple UI APIs (notablyScrollAreaprops,PageHeaderprops,ToolCall→Activitycomponent renames, and significant color token API changes).
Changelog
@mastra/core@1.72.0
Minor Changes
-
Added
maxRetriesandfailurePolicyoptions to the observational memoryobservationandreflectionsettings. WhenfailurePolicyis'continue', an Observer or Reflector model failure no longer ends the agent turn. (#24863)import { Memory } from '@mastra/memory'; const memory = new Memory({ options: { observationalMemory: { observation: { maxRetries: 2, failurePolicy: 'continue' }, reflection: { maxRetries: 2, failurePolicy: 'continue' }, }, }, });
-
Added support for resolving channel providers dynamically. The
channelsoption on theMastraconstructor now also accepts a resolver function, so channel connections added on the Mastra platform show up on a running server without a redeploy. (#25149)import { Mastra } from '@mastra/core/mastra'; import { channels } from '@mastra/connect'; export const mastra = new Mastra({ channels: await channels({ projectId: 'my-project' }), });
A resolver exposes its webhook and OAuth routes up front through
getRoutes(), and the providers themselves are resolved at runtime. Use the newmastra.resolveChannels()method to get the current provider map;getChannelProviders()keeps returning the latest resolved snapshot. Staticchannelsrecords keep working unchanged. -
Fence background task execution with a persisted, expiring ownership lease. (#24841)
Several managers can now share one storage without running the same task twice. A running task records which worker owns it and when that ownership lapses, recovery only reclaims a task whose lease has expired, and a worker that lost ownership can no longer save a result — so a stale run cannot overwrite the current owner's outcome.
Configure the lease length with
leaseDurationMs:const mastra = new Mastra({ backgroundTasks: { enabled: true, leaseDurationMs: 30_000 }, });
recoverStaleTasksOnStartkeeps its default oftrue, which is now safe because recovery is lease-fenced.Storage writes can additionally be made conditional on the current owner and lease:
await storage.updateTask( taskId, { status: 'completed', result }, { expectedStatus: 'running', expectedOwnerId: 'worker-1' }, );
The new write conditions are only enforced by storage adapters that understand them. On an older storage package the manager still runs, but the conditions are ignored and writes fall back to unfenced behaviour — so upgrade the storage package alongside
@mastra/coreto get the guarantee. -
Added an opt-in
summaryflag tolistWorkflowRuns. When set, storage adapters may return only{ status, timestamp }for each run snapshot instead of the full snapshot. (#25135)const { runs } = await workflow.listWorkflowRuns({ perPage: 20, page: 0, summary: true });
-
Storage adapters that support the new
trace-aggregatecapability can return aggregated trace results throughObservabilityStorage.aggregateTraces(), which executes aTrustedTraceAggregatePlanand returns aTraceAggregateResponse. The capability is declared alongsidetrace-query. Adapters that do not support it reject the call with aMastraErrorwhose id isOBSERVABILITY_STORAGE_AGGREGATE_TRACES_NOT_IMPLEMENTED. (#25124)import { parseTraceAggregateRequest, planTraceAggregate } from '@mastra/core/storage'; const plan = planTraceAggregate( parseTraceAggregateRequest({ timeRange: { from: '2026-08-01T00:00:00Z', to: '2026-09-01T00:00:00Z' }, measures: ['count'], }), ); const response = await storage.aggregateTraces(plan);
-
Added
detach()toDurableAgent.observe()so an observer can stop watching a run without affecting it. Leaving thefor awaitloop overfullStreamearly (break,return, or a thrown error) now also detaches, instead of keeping the subscription open for the life of the process. (#25048)const { output, detach } = await agent.observe(runId); request.signal.addEventListener('abort', detach, { once: true }); if (request.signal.aborted) detach(); for await (const chunk of output.fullStream) { send(chunk); }
cleanup()is unchanged. It still removes the run and its cached events, so only the process that owns the run should call it.Behavior change: when
idleTimeoutMsfires without anisAliveprobe, the observer now detaches, and the run's registry entry and cached events are kept. Before, a quiet but still-running run (for example, one waiting on a slow tool) was cleaned up. If you rely on the idle timeout to clean up runs whose process crashed, passisAliveso the run is only cleaned up when it reports the run is no longer running. -
Added an optional
sandboxIdto theWorkspaceSandboxinterface so hosts can read a provider's physical, reattachable sandbox id and persist it for deterministic reattach. (#24004)const id = sandbox.sandboxId ?? sandbox.id; // physical id when the provider exposes one
-
Added
CyberRefusalHandler, a processor that retries once when an OpenAI or Anthropic cybersecurity safeguard refuses ordinary work partway through an agent run. These refusals are often false positives, and nudging the model to continue usually gets past them. A second refusal on the same step is treated as genuine. (#25172)- OpenAI refusals (
cyber_policy, "This content was flagged for possible cybersecurity risk") fail the model call. Register the handler inerrorProcessors, beforeStreamErrorRetryProcessor. - Anthropic refusals stop the response with a
cyberclassifier refusal (content-filterfinish reason). Register the handler inoutputProcessors; the refused step is rolled back before the retry.
import { Agent } from '@mastra/core/agent'; import { CyberRefusalHandler } from '@mastra/core/processors'; const agent = new Agent({ id: 'coder', name: 'Coder', instructions: 'You are a coding agent', model: 'openai/gpt-5.6-sol', errorProcessors: [new CyberRefusalHandler()], outputProcessors: [new CyberRefusalHandler()], maxProcessorRetries: 3, });
Coding agents created with
createCodingAgentinclude it in both lanes by default, along with amaxProcessorRetriesbudget of 3 — output-step retries have no implicit default, so without a budget the Anthropic retry would be treated as an abort. - OpenAI refusals (
-
Added a
requireDescriptionoption to the workspaceexecute_commandtool. When enabled, the tool asks the model for a short plain-languagedescriptionof each command, listed beforecommandin the tool schema, so UIs can show it instead of the raw command. The option is off by default and the tool schema is unchanged when it is off. (#25032)const workspace = new Workspace({ sandbox, tools: { [WORKSPACE_TOOLS.SANDBOX.EXECUTE_COMMAND]: { requireDescription: true }, }, });
-
Added
runId,sessionId,userId, andorganizationIdfilters to advanced trace queries. Use them at trace scope or insidespans.some/spans.nonewitheq,ne,in,notIn,exists, andnotExists. Value discovery does not suggest these values, andorganizationIdcan only narrow the trusted tenant scope. (#24935)const result = await observability.queryTraces( planTraceQuery( parseTraceQueryRequest({ timeRange, where: { op: 'and', args: [ { op: 'eq', left: { path: 'userId' }, right: { literal: 'user-42' } }, { op: 'eq', left: { path: 'sessionId' }, right: { literal: 'session-9' } }, ], }, }), ), );
-
Added
withInitialHistorytosubscribeToThread. The subscription first emits a singlethread-historychunk with the stored thread messages, then only the retained parts that storage doesn't already cover, then live parts. Completed runs no longer replay on a retained backend such as Redis Streams, so chat UIs skip the replay animation and answered tool approvals aren't re-run. Pending approvals still come through so clients can prompt for them, and approvals already answered by a resumed run don't. Parts are compared to storage by when the model produced them, so slow publishing can't make stored parts appear again. (#24874)AgentControllersessions now subscribe withwithInitialHistory, so a session opened after a restart no longer re-acts on approvals from runs that already finished.The
thread-historychunk type only appears on subscriptions that passwithInitialHistory;agent.stream()and plain subscriptions are typed exactly as before.const subscription = await agent.subscribeToThread({ threadId, resourceId, withInitialHistory: { perPage: 40 }, }); for await (const chunk of subscription.stream) { if (chunk.type === 'thread-history') renderHistory(chunk.payload.messages); else applyLiveChunk(chunk); }
-
Fixed scheduled workflows hanging when started directly on serverless hosts (#18807). Declaring
scheduleon a workflow no longer switches it to the evented engine unless theMASTRA_WORKERSenvironment variable is set. Dev servers, single-process servers, and serverless hosts keep the default engine, so an HTTP-triggeredrun.start()runs in-process like any other workflow instead of waiting for a worker that never starts. (#25430)What changes
- Without
MASTRA_WORKERS, scheduled workflows run on the default engine and no longer need storage with concurrent-update support. Cron fires run in-process on the host that runs the scheduler. - With
MASTRA_WORKERSset to any value (split-worker deployments), scheduled workflows still run on the evented engine, and Mastra logs a warning to the console when one is created. SetMASTRA_WORKERSon every process in a split deployment, includingfalseon the API. scheduledeclared on Inngest workflows is ignored with a warning. Use Inngest'scroninstead.
import { createWorkflow } from '@mastra/core/workflows'; const dailyReport = createWorkflow({ id: 'daily-report', schedule: { cron: '0 9 * * *' }, // ... }).commit(); // Before: dailyReport.engineType === 'evented', and run.start() hung without workers // After (MASTRA_WORKERS unset): dailyReport.engineType === 'default' const run = await dailyReport.createRun(); await run.start({ inputData }); // completes in-process
To keep a scheduled workflow on the evented engine everywhere, import
createWorkflowfrom@mastra/core/workflows/evented. - Without
-
Added
PubSub.trimTopic(topic, { runId })(no-op by default). Once a thread run completes successfully and its messages are saved, Mastra deletes that run's entries from the thread topic, so retained backends no longer keep finished runs. Entries are matched byrunId, so a run resumed after a server restart also clears what it published before the restart. Runs still in progress, waiting for approval, or owned by another process are never touched. Threads whose agent has no storage are not trimmed. (#24875)Long runs are also trimmed as they go: each time messages are saved mid-run (by Observational Memory, or by
savePerSteponAgentandDurableAgent), the parts up to the last finished step are dropped from the topic via the newproducedBeforeoption. Pending approval and suspension prompts stay until the run itself is trimmed.Runs that fail, are canceled, or never start (for example a woken idle thread with no usable model credential) are deleted from the topic 30 seconds after they end. Nothing from them was saved, so reconnecting subscribers already ignore them; the delay lets subscribers reading live still see the failure.
Patch Changes
-
Fixed cancellation during session startup so cancelled requests do not reach the model or interrupt a newer turn. Added the provider finish reason to AgentController error events so clients can distinguish token limits and refusals from execution failures. (#24321)
Added a distinct session startup cancellation error so clients can handle interruptions without hiding provider or transport failures.
import { isSessionStartupCancelledError } from '@mastra/core/agent-controller'; try { await session.sendMessage({ content: 'Hello' }); } catch (error) { if (!isSessionStartupCancelledError(error)) throw error; console.log('Interrupted'); }
-
Fixed cost estimates for Anthropic 1-hour prompt-cache writes. The 5-minute and 1-hour cache-write token counts are now read from the provider's raw usage, so 1-hour writes are priced at their real rate instead of the cheaper 5-minute rate. (#25011) (#25089)
Also in: @mastra/observability@1.18.2
-
Fixed
textStreamandelementStreamreturning incomplete array elements when streaming structured output with an array schema. Previously, if an element arrived across several tokens, the streams could keep an early partial version (for example{"a":1}instead of{"a":1,"b":2}) that didn't match the finalobject. Each element is now emitted only once it is complete, so the streamed elements always match the final result. (#25278) -
Update provider registry and model documentation with latest models and providers (
e1c3193) -
Fixed tool approvals so they always go to the run that requested them. Approving a tool call after a newer run has started on the thread no longer resumes the wrong run. (#24874)
agent.sendToolApproval({ threadId, resourceId, runId, approved })resumes exactly the run you name, including after a server restart. If that run has already ended, the call throws instead of resuming a different suspended run on the thread. -
Fixed
DurableAgent.recover()failing withCannot read properties of undefined (reading 'messages')or(reading 'length')when a default-engine run was killed while a tool was running or between steps. Running checkpoints now keep the conversation state a restart reads back, while still trimming older history to keep storage small. (#25107) -
Fixed channel messages being dropped when
chatOptions.concurrencyusesburst,debounce, orqueue. Messages the Chat SDK batches together now reach the agent in the order they were sent and are saved to memory. Consecutive messages from one sender are merged into one turn; each sender's messages run as their own turn under that sender's identity. Custom channel handlers can read the batched messages fromcontext.skipped. Fixes #22496. (#25168)import { Agent } from '@mastra/core/agent'; import { createWhatsAppAdapter } from '@chat-adapter/whatsapp'; export const agent = new Agent({ id: 'support-agent', name: 'Support Agent', instructions: 'You are a helpful assistant.', model: 'openai/gpt-5-mini', channels: { adapters: { whatsapp: createWhatsAppAdapter() }, chatOptions: { concurrency: 'debounce' }, handlers: { onDirectMessage: async (thread, message, defaultHandler, context) => { console.log(`${context.skipped.length} earlier messages were batched`); await defaultHandler(thread, message); }, }, }, });
-
Fixed Channels tool approval cards so only the user who triggered the tool call can approve or decline it. In shared channels, clicks from other users are now ignored and logged instead of resuming the run. (#25165)
-
Fixed an unhandled EPIPE error that crashed processes using UnixSocketPubSub when the broker process exited before stream teardown. Unsubscribing after the broker is gone now completes cleanly, and a publish that races
close()now rejects with a clear "UnixSocketPubSub is closed" error instead of a raw socket error. (#25126) -
Fixed workspace
grepandlist_filesfailing with "No mount for path: .gitignore" when aCompositeFilesystemhas no root (/) mount. Reads of unmounted paths now report a not-found (ENOENT) error, so a missing root.gitignoreis treated as absent. (#25263) -
Fixed
restart()and automatic recovery re-running a workflow step that had already finished. If the process stopped right after a step completed but before the next one started, restarting the run executed that step again, and for loops, foreach, parallel blocks, and branches that meant repeating every item, arm, or iteration. Restart now continues from the next step, and a run whose last step had already finished completes with that step's saved output instead of crashing. Agent-loop checkpoints now keep the just-finished step's conversation so the next step still receives it after a restart. Fixes #24615. (#25114)DurableAgent.recover()no longer hangs when the run stopped after the agent had already sent its final answer. The recovered stream now finishes with that saved answer, andonFinishruns once. -
Fixed a claimed thread owner running an idle wake twice when a sender retried the same cross-agent message after its acceptance acknowledgement was lost. Such a retry now resolves to the outcome the first attempt already accepted — its run, or the reason it failed or was cancelled before it ran — instead of starting a second turn that repeats the turn's tool side effects. (#24774)
-
Fixed durable agents to reject invalid fallback timeout settings before preparation side effects. (#25188)
-
Fixed tool result objects with a
valuekey dropping sibling fields. (#25191) -
Fixed
DurableAgentignoringsavePerStep. Each step is now saved to memory before the run continues, as withAgent, so a durable run that is interrupted mid-way keeps the steps it had already finished. (#24875)await durableAgent.stream('Research and summarize', { memory: { thread: 'thread-1', resource: 'user-1' }, savePerStep: true, });
-
Durable agents now honor the
awaitedbackground-task disposition by keeping the model turn open until the authoritative tool result is persisted. Replayed workflow steps adopt the matching persisted task, resume or restart it according to its status, and reconcile terminal results instead of dispatching the tool again. (#24569)Also in: @mastra/inngest@1.10.1
-
Background tool results on the durable engine now apply configured transcript transforms, so payloads a user asked to redact are no longer persisted raw to thread history. They also use the configured Mastra
idGeneratorfor appended message ids instead of falling back to random UUIDs. (#24569)Also in: @mastra/inngest@1.10.1
-
Durable and evented agents now honor call-time
modelSettings.maxRetries. Previously the durable llm-execution step's retry ladder only read retry counts from serialized model-list entries (hardcoding 0 for single-model agents), so amaxRetriespassed viastream()/generate()model settings was silently ignored and failed requests were never retried. The agent-level retry config now rides on the serialized workflow options, and the ladder applies the same precedence as the in-process loop: an explicitly configured agent-levelmaxRetries(including 0) wins, otherwise the call-timemodelSettings.maxRetriesapplies. (#24569) -
Data chunks emitted by output processors via
writer.custom()are now persisted to thread history on the durable engine, matching the regular agent loop (#19375). Transient chunks remain stream-only. (#24569)Also in: @mastra/inngest@1.10.1
-
Provider-executed tool results that arrive in a later stream (tool call in one step, result in the next) are now committed to the transcript on the durable engine instead of staying stuck in the call state (ports #14282 to the durable agent loop). Configured transcript transforms also apply to these deferred results instead of being silently skipped. (#24569)
Also in: @mastra/inngest@1.10.1
-
Fixed durable agents mishandling provider-executed tools (like Anthropic web search). Calls whose result had not arrived yet no longer fail with ToolNotFoundError, and results are no longer committed twice, which previously overwrote their provider metadata. (#24569)
Also in: @mastra/inngest@1.10.1
-
Fixed durable agents looping until maxSteps when a model response ended with a terminal finish reason (content-filter refusal or length truncation) alongside a tool call. The loop now stops instead of re-sending the same request and re-triggering the same refusal (ports #17893 to the durable agent loop). (#24569)
Also in: @mastra/inngest@1.10.1
-
Serialize
modelSettings.timeoutinto the durable workflow input.serializeModelSettingsdropped thetimeoutfield entirely, so a durable run's execution time budgets were lost across the serialization boundary: cold recovery (DurableAgent.recover()) could not restore the run-leveltotalMsbudget — a restarted run lost its configured deadline and ran unbounded — and the per-callstepMs/firstChunkMsbudgets never reached the durable llm-execution step's shared execute wrapper. All three subfields now survive serialization (positive finite numbers only), and a recovered session re-arms the persistedtotalMsbudget. (#24569) -
Fixed
createDurableAgentdropping non-transientdata-*parts that tools write withcontext.writer.custom(). These parts are now saved to memory, just like with a regularAgent. Transient parts still only go to the stream. (#25283) -
Output processors'
processToolResulthooks now run on the durable engine. Previously they were skipped entirely, so a redaction processor had no effect on durable streams or transcripts. Processor mutations and tripwire blocks now apply to the emitted chunk and the persisted transcript, before the raw value can reach subscribers. (#24569)Also in: @mastra/inngest@1.10.1
-
The run-level
modelSettings.timeout.totalMsbudget is now enforced on durable agents (ports #21724 to the durable agent loop). Previously only the per-call budget applied, so a hanging provider or a long tool chain could run forever. The total budget now bounds the whole run, is re-armed with the original value on cold resume and recovery, and expiry surfaces as a run error rather than a silent stop. (#24569)Also in: @mastra/inngest@1.10.1
-
Tool payload transforms now persist their state on the durable engine. Transcript-target transforms correctly redact stored args and results in thread history; previously only the streamed chunks were transformed and raw values were silently persisted. (#24569)
Also in: @mastra/inngest@1.10.1
-
Fixed DurableAgent dropping cached-input, cache-write, and reasoning token counts from
model_generationspan usage, so tracing exporters and cache/reasoning token metrics now match the regular Agent. (#25392) -
Durable runs of stored agents now stay pinned to the agent version they started on. Both resuming a suspended run (#22128) and recovering a crashed run re-resolve the original pinned version instead of silently switching to the latest published version. If the pinned version no longer resolves, the run falls back to the current definition with a warning. (#24569)
Also in: @mastra/inngest@1.10.1
-
Fixed saved dynamic workflows losing
.map()entries that useinitData: true. These mappings now keep reading from the workflow's initial input after the workflow is saved and loaded again, the same as they do in memory. Workflows saved before this fix need to be saved again. (#25152) -
Fixed
Session.sendMessage()hanging forever when its run never produced a completion event, for example when several turns started at once on the same thread. It now rejects if the session's stream processing fails, and resolves if the run is aborted or the session's thread subscription is torn down. See #25140. (#25375) -
Fixed delegation bail() taking one extra model turn on the evented engine. When an onDelegationComplete hook calls ctx.bail(), the supervisor loop now stops in the same iteration on every engine. Previously the bail signal was passed between workflow steps by mutating a shared request context, which only works when all steps run in one process — on the evented engine each step gets its own copy, so the loop made one more model request before stopping. The bail signal now travels on the tool call step's serialized output, which crosses process and event boundaries reliably. The Inngest agentic loop's continuation predicate now also honors this flag, so bail() stops Inngest-hosted supervisor loops in the same iteration too (previously it only stopped via maxSteps). (#24569)
Also in: @mastra/inngest@1.10.1
-
Propagate the caller's actor identity through the evented workflow engine. Previously the evented engine accepted
actorat the API surface (used for the fine-grained-authorization gate) but dropped it before execution:execute()params omittedactorand the workflow event payloads carriedrequestContextbut notactor, so steps, tools, and loop conditions running on the evented engine never saw the caller's identity — actor-based authorization inside tools silently saw no actor. The actor signal now rides the event envelope alongsiderequestContextacross every hop (start, resume, restart, time travel, step events) and reaches step, tool, agent, and condition execution contexts, matching the default engine. (#24569) -
EventedAgent runs on the built-in evented workflow engine again. It previously fell back to the default in-process engine because the evented path crashed on resume and recovery. Those bugs are fixed, and the engine is now covered by the durable-agent conformance suite — including crash recovery: a fresh process over the same storage can resume in-flight runs via
recover(runId)/recoverActiveRuns(). (#24569) -
Fixed evented workflow snapshots growing quadratically with
.foreach()input size. Each iteration's progress record no longer stores a copy of the whole input array, so large foreach runs no longer produce huge snapshots that can stall storage. Fixes #24943. (#25376) -
Durable agents no longer fail to start when their storage cannot apply concurrent workflow updates atomically. (#24569)
The evented execution engine advances a run from concurrent workers, so it refuses to start on a storage adapter whose workflows domain does not support atomic concurrent updates. Durable agents now degrade to the default in-process engine with a warning in that case, the same way an agent with no Mastra host already did, rather than throwing on the first
.stream()call. This keeps agents on adapters such as@mastra/redisand@mastra/valkeyworking, at the cost of evented execution — the warning names the adapter and the capability it is missing.A workflow that opts into the evented engine directly, by declaring a
schedule, still gets an error, because there is no other engine it could have meant. That error now names the storage adapter it came from, names the missing capability, and lists the adapters that support it; it previously only suggested removing theschedulefield. -
Fixed a redelivery race on the evented engine where a
workflow.step.runevent redelivered by an at-least-once transport after the step had already suspended would re-execute the step and overwrite the stored suspended record, stripping the suspendPayload that resume recovery depends on. The mid-step pre-write now drops a non-resume delivery when both the run and the target step record are suspended — a state in which no legitimate delivery exists — so the suspended record and its resume artifact survive redelivery. The check is a read-then-write that narrows the race window; fully closing it would need compare-and-set support onupdateWorkflowResultsacross storage adapters. (#24569) -
Fixed evented workflows failing the entire run when a
.branch()condition throws. A throwing condition is now logged and treated as false, so the remaining branches still run — matching the default workflow engine. (#25279) -
Fixed DurableAgent and EventedAgent treating falsy primitive resume payloads (
false,0,'') as "no resume data" in the durable tool-call step. Suspended background tool tasks resumed with a falsy payload were stranded while a duplicate task was dispatched. The durable loop now uses the same nullish check as the regular agent loop (#22363), so onlyundefined/nullmean "not resuming". (#24569) -
Fixed structured output on durable agents using the evented engine. Schemas passed as
structuredOutput.schemanow stay available on later model calls and after a run is recovered. Workflow options that cannot be saved now fail with an error naming the field. (#25075) -
Stored agents backed by FilesystemStore now retain durable execution settings. (#25123)
-
Fixed
foreachover a nested workflow failing with "already resumed by another caller" when one iteration suspended while others were still starting. Each iteration now starts its own child workflow run, and suspended iterations can be resumed individually withforEachIndex. (#25394) -
Added atomic cross-process thread ownership handoff (#24898)
agent.claimThreadOwnership()now acceptsyieldOwnership,onOwnershipYielded, andonOwnershipLostcallbacks. When another process asks for a thread andyieldOwnershipreturnstrue, the thread passes to that process in the same request.onOwnershipYieldedruns after the handoff. Owner lookups for message delivery never callyieldOwnership. If another process takes an expired claim first,onOwnershipLostruns so the caller can retry.UnixSocketPubSubnow coordinates thread ownership between processes that share a socket directory. A running owner restores an expired claim when no other process has taken it. When an owner hands off a thread or exits, another process can take over immediately.Before
const claim = await agent.claimThreadOwnership({ resourceId, threadId, });
After
let claimActive = true; const claim = await agent.claimThreadOwnership({ resourceId, threadId, yieldOwnership: () => session.thread.getId() !== threadId, onOwnershipYielded: () => { claimActive = false; }, onOwnershipLost: () => scheduleClaimRetry(threadId), });
-
Fixed durable agents crashing during crash recovery when the process was restarted while an LLM call was in flight. The active step now restarts from its own preserved input instead of a completed predecessor's pruned output, which previously caused "Cannot read properties of undefined (reading 'messages')" (#22636). (#24569)
-
EventedAgent tool-approval, tool-denial, and in-execution resume no longer fail with "Cannot read properties of undefined (reading 'messages')": the evented engine retains running conversation history in persisted agent-loop snapshots, since its storage-merged step results are live data (unlike the default engine's write-only snapshots) (#24569)
Also in: @mastra/inngest@1.10.1
-
Fixed Gemini 3 rejecting the next request with "Corrupted tool call context" when native Google Search (
webSearchTool) and another tool (such as a skill) were called in the same step. The provider-executed flag from the tool call is now kept when its result arrives without one, so the search call and its result stay together in the conversation history. (#24888) -
A paused goal now says why it paused.
/goal statusand the goal modal show the cause — evaluation budget exhausted, or a judge that failed to evaluate — and it survives a reload instead of disappearing after the moment it happened. Resuming or completing a goal clears the cause, so a later pause never shows a stale one. (#24618)Also in: @mastra/code-sdk@1.9.0
-
Removed the unused
executeDurableToolCallshelper and itsToolExecutionContext/ToolExecutionErrortypes from@mastra/core/agent/durable. This code was never wired into any agent loop; both the durable and regular loops execute tools through their own step implementations. (#24569)Also in: @mastra/inngest@1.10.1
-
Fixed
<PROVIDER>_BASE_URLbeing ignored for providers such as Google, xAI, Perplexity, Cerebras, DeepInfra, Together AI, and Vercel. SettingGOOGLE_BASE_URL(or the equivalent for other providers) now routes requests and API keys through your proxy or gateway instead of the public provider host. (#25354) -
Approval prompts are now independent per thread and run. Concurrent and detached approvals stop overwriting, stranding, or answering each other's gates: gates are keyed by tool call and tagged with the thread and run that opened them, abort and user-message interjection release only the current thread's gates, detached-thread approvals no longer fire notifications or permission hooks, and an approved run resumes against the thread, run, resource, agent, and cancellation signal that actually parked it — so a mode switch, resource re-scope, or a successor run cannot redirect or cancel the parked continuation. (#24776)
Approval responses now require the gate's
toolCallId. Without the id, a response could release whichever gate happened to be parked — another thread's or another run's — so an id-less response is rejected instead of silently applied. Every caller inside the repo already passes the id; this only affects external callers of the session API.Before:
session.respondToToolApproval({ decision: 'approve' });
After:
session.respondToToolApproval({ decision: 'approve', toolCallId });
Similarly,
SessionApproval.arm()now requires atoolCallId, andgetToolCallId()is replaced bygetToolCallIds()(which accepts an optional thread/run filter)."Always allow" grants are now scoped to the gate's thread. Approving with
always_allow_categorygrants the tool's category to the thread that owns the gate rather than the whole session, so a background thread's approval cannot widen what the current thread may run without prompting.grantCategory,grantTool,hasCategoryGrant,hasToolGrant, andgetGrantsaccept an optionalthreadId; passing one scopes the grant to that thread, omitting it keeps the existing session-wide behavior for embedders that grant explicitly.Also in: @mastra/code-sdk@1.9.0
-
Fixed cross-agent wakes with
requireClaimedOwnerfailing with "No claimed thread owner responded" when the owning agent was alive but slow to answer (for example on a busy host or CI). Owner discovery for wakes now retries with growing timeouts for up to 1 second before giving up, and the error states how long it waited. (#25488)Both limits can be tuned with environment variables:
MASTRA_AGENT_THREAD_OWNER_DISCOVERY_TIMEOUT_MS— per-attempt wait (default100)MASTRA_AGENT_THREAD_WAKE_OWNER_DISCOVERY_DEADLINE_MS— total wake discovery budget (default1000)
-
Fixed follow-up DeepSeek requests failing in thinking mode after switching providers or using tools. (#25189)
-
Added an
outputEncodingoption toLocalSandboxso command output in non-UTF-8 encodings is decoded correctly. On Windows systems using a legacy code page, such as Chinese (GBK / code page 936), native commands previously returned garbled text. (#25266)const sandbox = new LocalSandbox({ outputEncoding: 'gbk' });
UTF-8 remains the default. Fixes #25249.
-
Fixed agents with working memory seeing the word "null" in their instructions when no working memory had been saved yet, for example on the first message of a new thread. The agent now sees "No working memory data available." instead. Fixes #23724. (#25057)
-
Fixed
run.restart()on the default workflow engine after a process crash during a nested workflow resume. Restart no longer fails with "This workflow run was not suspended", and the resumed step keeps its resume data and completes instead of suspending again. Fixes #25187. (#25234)Fixed
timeTravel()ignoring resume values such asfalseor0. -
Evented workflows now persist the running step record and routing state before executing each step, so runs killed mid-step recover via restart() instead of failing with "Execution path is empty" (#24569)
Also in: @mastra/inngest@1.10.1
-
Fixed CachingPubSub dropping events that arrive from another caching tier. When a durable agent's stream cache follows a source bus that is itself a CachingPubSub with its own cache (for example a user-supplied CachingPubSub passed to new Mastra({ pubsub })), events from that tier already carry an index and were treated as local echoes and discarded — they never reached the agent's subscribers and could not be replayed. The follower now only treats indexed events as echoes when the source shares the same transport; foreign-indexed events are cached under a fresh index and republished so live delivery and replay both work across tiers. (#24569)
-
Fixed durable agent resumes (including
InngestAgent) running another agent's tool when two agents register tools with the same id. On a cold worker, tool calls now resolve against the run's own agent before falling back to Mastra-wide tools. (#25393) -
Deprecated Observational Memory
scope: 'resource'in the memory config types. Editors now show thescopeoption as deprecated. Remove it to use the default thread scope, and enableretrievalor resource-scoped working memory for cross-thread continuity. (#24933) -
Fixed evented agent streams losing events when the agent uses a custom pubsub. The evented workflow engine publishes run events on the Mastra instance's pubsub, but the agent's stream listened only on its own pubsub — with a custom pubsub configured, suspend, finish, and tool events silently never reached the stream. The agent's caching pubsub now also follows the Mastra instance's pubsub as a source, so evented runs stream correctly regardless of which pubsub the agent was configured with. (#24569)
-
Fixed tool approvals for Inngest durable agents (#25154). (#25167)
- Runs created with
createInngestAgent()that wait on tool approval now appear inGET /api/agents/:agentId/suspended-runsandagent.listSuspendedRuns(). approve-tool-callanddecline-tool-callnow accept these runs. They no longer return "Access denied: durable run belongs to a different resource".- Durable runs waiting on an approval-gated tool now report
requiresApproval: trueand the tool'sargs.
Also in: @mastra/server@1.72.0
- Runs created with
-
Fixed a race where a background tool result could be saved to memory without its configured transcript transform applied. When a background task finished while the turn was still being persisted, the memory history save could land last and store the raw tool result instead of the redacted one. All persistence paths now apply the same transcript redaction, so raw payloads never reach storage. (#24569)
-
Fixed threads getting stuck on Anthropic and Bedrock after a step failed mid-reasoning. Prompts now leave out reasoning that the provider cannot accept, so later turns no longer fail. (#24928)
-
Fixed model fallback logging so recovered failures no longer page as errors. When a model in a
modelschain fails and another model is still available, Mastra now logs a single warning naming the failed and next model instead of two error-level records. Failures of the last model in the chain are still reported as errors. (#25489) -
Processor retry feedback no longer invalidates the provider prompt cache. When a processor calls
abort(reason, { retry: true }), the reason used to be added as a system message ahead of the conversation, so the retry and later steps missed the cache for the whole history. The reason is now added as a system reminder after the conversation, so each retry only adds to the previous request. (#25171)The reason is sent verbatim as
<system-reminder>{reason}</system-reminder>. It is no longer wrapped in the[Processor Feedback] Your previous response was not accepted: … Please try again with the feedback in mind.template, so write the reason as the instruction you want the model to follow. The reminder is kept in thread history as a system-reminder signal, which default memory recall hides, like other processor reminders. -
Documented two
aggregateTraces()result rules: null dimension values sort last in both sort directions, and a time range with no matching traces returnsrows: []rather than a single zero-valued row. (#25016) -
Fixed
structuredOutput.jsonPromptInjection: falsebeing ignored when a structuringmodelis set. The main model's prompt no longer receives the full JSON schema on every step when you opt out of injection. (#25478) -
Fixed tool search loading every
search_toolshit whenautoLoadisfalse. Onlyload_toolnow activates tools, so the tool list stays stable and the prompt cache is preserved across steps. (#25076) -
Fixed generated thread titles re-creating a thread that was deleted while the title was still being generated. Deleted threads now stay deleted, and the title save no longer overwrites thread metadata that changed during generation. (#25385)
-
Fixed agent runs with
readOnlymemory still persisting their transcript when a background tool task completed. The background-result flush now honors the readOnly contract the same way the suspension flush already did. (#24569) -
Fixed durable and evented agents to reject invalid model timeout settings before starting a run. (#25079)
-
Fixed thread subscribers showing a run as idle after a tool approval. When a run resumes, thread subscribers now get a
startevent for the resumed part. This applies to subscribers that were already listening and to those that join while the run resumes, so the UI shows the run as running again and can cancel it. (#24875) -
Fixed
createRunCommandToolsecurity checks on Windows.allowedBasePathsnow accepts working directories with backslash paths (previously every subdirectory was rejected), command allow/block lists now recognize Windows paths such asC:\tools\rm.exe, and the unsafe-character filter now rejects dangerous input consistently on every call. (#25383) -
Added a
getBasePathoption toAgentsMDInjector. When set, relative tool paths resolve against that directory instead ofprocess.cwd(), andAGENTS.md/CLAUDE.mdfiles outside it are never injected. (#25071)new AgentsMDInjector({ getBasePath: () => '/path/to/checkout', });
-
Fixed Anthropic extended-thinking threads with working memory getting stuck on every turn with "thinking blocks in the latest assistant message cannot be modified". When the model spent a step only on an
updateWorkingMemorycall, the saved history kept that step's thinking without its tool call, so later requests replayed it merged into the next step. That step is now dropped when the call is hidden. (#24924)Threads already saved in this state recover when
ProviderHistoryCompatis in the agent's error processors: on that Anthropic error it drops the leftover thinking step and retries once.import { Agent } from '@mastra/core/agent'; import { ProviderHistoryCompat } from '@mastra/core/processors'; const agent = new Agent({ // ... errorProcessors: [new ProviderHistoryCompat()], });
Fixes #22798.
-
Fixed the durable agent engine leaking raw tool output when an output or result processor throws. Previously, if a processor (for example a redaction processor) failed with a non-tripwire error, the durable engine would warn and continue with the unprocessed value: the raw tool result was still emitted on the stream and persisted to the transcript. Both paths now fail closed — a throwing stream processor suppresses the chunk instead of emitting the original, and a throwing tool-result processor substitutes an error placeholder for both emission and persistence. The run itself stays alive in both cases. This matches the regular loop, which already refused to emit or persist raw values when a processor throws. (#24569)
-
Fixed durable agents looping until maxSteps when a provider ends the stream with finishReason 'other' and no output. The empty response now surfaces as a stream error after one attempt, and completion checkers no longer grade errored iterations (ports #22273 to the durable agent loop). (#24569)
Also in: @mastra/inngest@1.10.1
-
Fixed subagents without memory failing immediately when their workspace has a browser. These subagents now complete their tasks and still see browser context in their prompt. (#25291)
-
submit_planresults now record the reviewer's decision.submittedPlan.actionisapprovedorrejected, andsubmittedPlan.feedbackholds the reviewer's comments when they asked for changes. UIs can show the outcome from message history without parsing the result text. (#25413) -
Fixed semantic recall running messages from other conversations together on a single line. Each recalled message now appears on its own line in the system prompt, so the model can tell where one message ends and the next begins. (#25059)
-
Fixed durable tool payload transforms after cross-process recovery. (#25387)
-
Added the assistant message id to tool-input events so consumers can attribute streamed tool arguments to the model step that produced them. (#25196)
-
Added
withInitialHistoryto thread subscriptions over HTTP. Pass it tosubscribeToThreadin@mastra/client-js, or touseChatin@mastra/react, to get stored thread messages as onethread-historychunk before live parts. Completed runs no longer replay on reconnect. (#24876)const subscription = await agent.subscribeToThread({ threadId, resourceId, withInitialHistory: { perPage: 40 } });
const chat = useChat({ agentId, threadId, resourceId, enableThreadSignals: true, withInitialHistory: true });
Agent controller tool-approval requests now resume the stored suspended run when no approval is waiting in the session, so approval cards restored from thread history after a restart can be answered.
Also in: @mastra/client-js@1.51.0, @mastra/react@1.7.0, @mastra/server@1.72.0
-
Fixed DurableAgent streams to hide response metadata chunks and attribute finish chunks to the agent. (#25078)
-
A function-form
toolDisplayon a channel adapter can now render the approval card's final state after a user clicks Approve or Deny. Previously, Mastra always replaced a custom approval card with its own English "Approved" or "Denied" text, which also showed the tool's internal registry key. (#25486)The function now receives
approvedanddeniedevents (deniedincludesbyUser). Return{ kind: 'post', message }to replace the card:channels: { adapters: { slack: { adapter: createSlackAdapter(), toolDisplay: event => { if (event.kind === 'approved') return { kind: 'post', message: 'Onaylandı' }; if (event.kind === 'denied') return { kind: 'post', message: `Reddedildi: ${event.byUser ?? ''}` }; return undefined; }, }, }, },
If the function returns nothing, a blank message, or throws, the default card is shown.
If your function switches exhaustively on
event.kind, add cases for the two new kinds:case 'approved': return { kind: 'post', message: 'Approved' }; case 'denied': return { kind: 'post', message: `Denied${event.byUser ? ` by ${event.byUser}` : ''}` };
-
Fixed a TypeScript error when passing tools made with
createTooltonew Mastra({ tools })in projects that enableexactOptionalPropertyTypes. You no longer need a cast to register these tools. (#25064) -
Tool invocation parts now record
updatedAtwhenever their state changes (approval response, result, error), andtool-call-approvalchunks carry the matchingupdatedAt. Compare them to tell whether a streamed approval is newer than the stored tool call. (#24874)for await (const chunk of subscription.stream) { if (chunk.type !== 'tool-call-approval') continue; const stored = storedParts.get(chunk.payload.toolCallId); if (stored?.updatedAt && chunk.payload.updatedAt <= stored.updatedAt) continue; // already stored promptForApproval(chunk); }
-
Corrected the
ToolSearchProcessorsearch.minScoredocumentation. Scores are raw BM25 relevance plus name-match boosts and can exceed 1, rather than falling in a 0-1 range. (#25498) -
The evented workflow engine now honors
emitStepEvents: false(step-lifecycle watch events are suppressed instead of the option being silently ignored) and fails loudly whencreateRun()is called on an evented workflow that has no registered Mastra host, instead of starting a run that hangs or fails deep inside the event processor. Run-level events and execution routing are unaffected by the step-event gate. (#24569) -
result.textnow keeps the answer when an output processor trips the wire. It matchesresult.steps[].text,getFullOutput().textandresult.response.messages. Checkresult.tripwireto see whether the output was rejected. (#25483)const result = await agent.generate('...'); if (result.tripwire) { // result.text still holds the rejected answer for logging or review }
-
Fixed channel handler logs to preserve serialized error details and correlation. (#25349)
-
Fixed the TypeScript type for DurableAgent stream options to include providerOptions. Provider-specific options (like OpenAI reasoning settings) were already forwarded at runtime, but TypeScript rejected them when passed to stream() or generate() on a durable agent. (#24569)
-
Fixed suspended tool calls losing their suspension details when stored messages are converted to AI SDK v6 or v7 UI messages.
toAISdkMessages(messages, { version: 'v6' })and{ version: 'v7' }now include thedata-tool-call-suspendedpart (with the suspend payload and resume schema), matching the v5 output, so suspended tools stay resumable after a page reload. (#25134) -
Fixed
requestContextSchemabeing silently ignored by durable and evented agents.DurableAgent.stream()(and evented subclasses) now validate the request context against the agent'srequestContextSchemabefore starting execution, matchingAgent.stream()andAgent.generate(). Invalid or missing context values now reject with the same validation error, instead of the run starting with unvalidated context. (#24569) -
Fixed evented agent streams hanging forever when a run fails before its first chunk. A workflow-level failure now surfaces as an error on the stream instead of leaving the consumer waiting. (#24569)
Also in: @mastra/inngest@1.10.1
-
Fixed AgentController not showing live progress for subagents delegated through
Agent.agents. These subagents now appear indisplayState.activeSubagentswhile they run, with their tool calls and text streaming in, instead of only showing up once they finish. Fixes #25019. (#25424) -
Fixed trajectory scorers in dataset experiments receiving an empty trajectory for workflow targets when no trace is stored. They now receive the workflow's executed steps, matching
runEvals. (#25269)
@mastra/auth-workos@1.6.6
Patch Changes
- Fixed cookie sessions losing their organization on re-authentication. With
fetchMemberships: true, a cookie session that has no selected organization now keeps the organization of the user's only membership. A selected session organization still takes precedence, and users with no memberships or several stay without an organization. (#25264)
@mastra/clickhouse@1.22.0
Minor Changes
-
Added
runId,sessionId,userId, andorganizationIdfilters to advanced trace queries at trace scope and insidespans.some/spans.none. No migration is needed. (#24935)Also in: @mastra/duckdb@1.12.0, @mastra/pg@1.28.0
Patch Changes
- Fixed the Studio Traces page running out of memory or timing out on large ClickHouse databases. Trace queries no longer read every stored trace's full data before applying the selected time range, which cuts memory use and data read on large tables. Fixes #25141. (#25225)
@mastra/client-js@1.51.0
Minor Changes
- Added a
summaryoption toworkflow.runs()to list runs without downloading full snapshots:workflow.runs({ summary: true }). (#25135)
Patch Changes
- Fixed workflow run methods failing with
404 Workflow run not foundwhen the run id contains characters like+,&,#or/.start,resume,restart,timeTravel(and their async/stream variants),cancel,runByIdanddeleteRunByIdnow URL-encode the run id, matchingcreateRunandstartAsync. (#25338)
@mastra/cloudflare@1.7.0
Minor Changes
-
Persist background task ownership so recovery can be fenced on the lease. (#24841)
Adds
ownerIdandleaseExpiresAtto the background tasks schema and honours the newexpectedOwnerId/expectedLeaseExpiresAtwrite conditions onupdateTask(). Existing tables are migrated in place; rows written before the upgrade carry no ownership and are treated as reclaimable.Convex deployments must redeploy their schema and server functions: the new fields are declared as optional, but documents that carry them are rejected until the schema is pushed.
Also in: @mastra/cloudflare-d1@1.4.0, @mastra/convex@1.6.0, @mastra/dynamodb@1.4.0, @mastra/lance@1.4.0, @mastra/libsql@1.24.0, @mastra/mongodb@1.20.0, @mastra/mssql@1.10.0, @mastra/mysql@0.11.0, @mastra/pg@1.28.0, @mastra/spanner@1.9.0, @mastra/upstash@1.5.0
@mastra/code-sdk@1.9.0
Minor Changes
-
Added
@mastra/code-sdk/scheduleswith a process-localThreadSchedulerthat sends recurring prompts into a thread on wall-clock boundaries, following daylight saving changes like cron. (#25476)scheduler.create( { prompt: 'Check whether the build is green', trigger: { kind: 'every', interval: { ms: 5 * 60_000, label: '5m' } }, }, { threadId, resourceId }, );
A schedule's source is a prompt, a prompt file re-read on each fire, or a script whose output becomes the prompt.
run()reports whether a manual fire was delivered, failed, skipped because it was already firing, or not found. The Mastra Code controller exposes the scheduler asthreadScheduler, andcreateScheduleTools()builds agent tools for it, enabled with thescheduleToolsconfig option or thesignals.experimentalScheduleToolssetting (off by default). -
Added model, mode, and reasoning selection for Agent Client Protocol clients: (#24321)
await connection.setSessionConfigOption({ sessionId, configId: 'thought_level', value: 'high', });
Fixed user prompts and internal system messages appearing as assistant replies. Failed turns now return errors that clients can display; token limits and refusals return their corresponding protocol stop reasons.
Each conversation now has its own runtime and uses the client's requested working directory. Client-supplied stdio and HTTP MCP servers connect with their configured environment variables or headers. Unsupported legacy SSE servers return an error before session startup.
Tool approvals and sandbox access requests now ask the client for permission. Cancelling a session stops its active turn and queued prompts without interrupting other conversations. Shutdown waits for cleanup even when multiple signals arrive.
Headless runs report session cancellation during startup as aborted while preserving provider and transport errors. Session creation retains its original error even if cleanup also fails.
Model lists now omit unconfigured providers and retain full routing IDs in labels, preventing gateway entries from appearing to be direct provider models.
Added workspace skill discovery and invocation for Agent Client Protocol clients. Clients can list user-invokable skills and activate them with an ordinary prompt:
await connection.prompt({ sessionId, prompt: [{ type: 'text', text: '/skill/review Check the current changes' }], });
Skills marked
user-invocable: falseare hidden and cannot be invoked through slash commands.
Patch Changes
-
Mastra Code now retries once when OpenAI or Anthropic cybersecurity safeguards refuse ordinary coding work partway through a run, instead of stopping the run with a refusal error. (#25172)
The behavior needs no configuration:
import { mountAgentControllerOnMastra } from '@mastra/code-sdk'; const { mastra, controller } = await mountAgentControllerOnMastra({ cwd: process.cwd() });
-
Fixed cross-agent thread ownership so peers advertise the mastracode instance that currently owns the thread instead of an instance that only visited it earlier. (#24898)
Sessions still keep every loaded thread advertised so saved peers remain reachable after
/new. When another instance requests a thread that is no longer current, the SDK now transfers its lease during that claim attempt and forgets the yielded advertisement. A thread that is still current is retained, and retries succeed as soon as its current owner moves away. This prevents stale titles, missing peers, timeout races during event-loop stalls, and release-before-reclaim gaps. -
Fixed: mastracode no longer writes session scorer results into the local mastra.db. (#23632)
The outcome and efficiency scorers run on every session and stored a result row each time, but nothing in mastracode ever read those rows back, so they only made the database grow. Scorer results are now discarded after each run instead of being kept on disk or in memory.
SDK change:
createMastraCode().storage.getStore('scores')now returns empty results from lookups and listings, including for scores already in the database. To read those older scores, open the database directly.Before:
const { storage } = await createMastraCode(); const scores = await storage.getStore('scores'); const { scores: rows } = await scores!.listScoresByRunId({ runId, pagination: { page: 0, perPage: 50 } });
After:
import { LibSQLStore } from '@mastra/libsql'; import { getDatabasePath } from '@mastra/code-sdk/utils/project'; const store = new LibSQLStore({ id: 'mastra-code-scores', url: `file:${getDatabasePath()}` }); const scores = await store.getStore('scores'); const { scores: rows } = await scores!.listScoresByRunId({ runId, pagination: { page: 0, perPage: 50 } });
Part of #22056.
-
Fixed GitHub plugin installs failing when
package.jsondeclares pnpm with a Corepack integrity hash, such as"packageManager": "pnpm@12.6.0+sha512.<hash>"(the format written bycorepack use). The hash is now accepted and passed to Corepack, which verifies the downloaded pnpm against it. (#25121) -
Quiet mode now shows a short description of each shell command, like
Drilling into the failed CI job, instead of the raw command, so you can follow what the agent is doing without reading long commands and scripts. The agent is asked to write the description first and to phrase descriptions as a running narrative across commands. The description streams in as the agent writes it, and the raw command never flashes first. (#25032)Consecutive shell calls in the same directory share one compact box. When the quiet mode tool preview lines setting is 1 or more, the latest output streams into a shared preview at the top of the box:
╭──────────────────────────────────────────────────────────╮ │ Test Files 3 passed (3) │ │ Tests 42 passed (42) │ ├──────────────────────────────────────────────────────────┤ │ $ ~/code/my-project │ ├──────────────────────────────────────────────────────────┤ │ ✓ Listing later commits touching the sandbox code 101ms │ │ ✗ Checking the release tag 1.5s │ │ └▸ fatal: ambiguous argument 'v1.68.0..HEAD' │ │ ⠋ Running the sandbox test suite 4s │ ╰──────────────────────────────────────────────────────────╯Each directory gets its own box (subdirectories of the project show as
./path), running commands show a spinner and a live timer, failed commands show their error line, and background commands are marked as started. With preview lines set to None, the box has no preview. Ctrl+E still reveals the full command and output. -
Added
mastracode prune, which cleans up the local database from the shell instead of from inside the interactive session. (#23632)It deletes data older than the retention policies, and
--vacuumreturns the freed space to the operating system for a local libsql database (remote libsql and Postgres only delete rows).--keep-memorykeeps chat history. This works even when the interactive session will not start, which previously left a large database with no way to reclaim it from inside the tool.mastracode prune # delete rows past the retention policies mastracode prune --vacuum # ...then compact the files to reclaim disk mastracode prune --keep-memory # ...but keep chat history
mastracode pruneand/prunerefuse to run while another session is open, and a session started during either waits for it to finish. The lock lives in the app data directory, so sessions sharing oneMASTRA_DB_PATHwith differentMASTRA_APP_DATA_DIRvalues don't see each other.Part of #22056.
-
A routine goal save could delete a live goal. Saving used to double as deleting whenever no goal happened to be loaded in memory, so an empty ordinary save could remove the objective without explicit deletion intent. Saving now deletes only after an explicit clear;
/goal clearstill removes the goal and its older stored copy. If that delete fails,/goal clearnow reports that the goal may still be active instead of claiming success, and switching back to the thread retries the delete rather than silently restoring the goal. (#24618)For anyone using
GoalManagerfrom@mastra/code-sdkdirectly:clear()followed bysaveToThread()on the same thread still deletes the goal. The only change is that a save with no goal loaded, and noclear()before it, no longer deletes.deleteFromThreadis also available to delete directly, and resolves to whether the delete landed:await goalManager.deleteFromThread(state);
-
Fixed agents receiving the host app's
AGENTS.mdwhen working in a different repository (for example, Mastra Factory runs). Instruction-file reminders now resolve relative paths against the session's project path and only load instruction files from inside that checkout. (#25071)
@mastra/codemod@1.1.5
Patch Changes
-
Fixed the v1 evals codemod to migrate legacy scores imports. (#25108)
-
Fixed v1 message type migrations to use valid agent and memory exports. (#25104)
-
Fixed large codemod previews so printed and verbose output completes without hitting the process buffer limit. (#25244)
@mastra/connect@0.5.0
Minor Changes
-
Added multi-connection support and a string-array shorthand to
@mastra/connect. (#24864)Multi-connection support: when a provider has more than one active connection, tools are wrapped with a required
connection_nameinput and a new<provider>__list_connectionstool is exposed so agents can discover and select which connection to use. Single-connection behavior and explicitconnectionIdpins are unchanged. The wrapper preserves each inner tool's approval contract (requireApprovaland, when the MCP server-level policy is a function,needsApprovalFn) so approval prompts still fire for multi-connection MCP tools; missing or unknownconnection_namefails closed and requires approval.The helper key uses a double underscore (
<integrationId>__list_connections) so it cannot collide with a real provider tool of the form<integrationId>_<toolName>(for example, WorkOS ships a realworkos_list_connectionstool that lists SSO connections).Previously, multi-active provider connections were skipped with a warning.
// connect() returns a resolver; call it to load tools. const tools = await connect({ integrations: ['linear'] })(); // Agent lists available connections. await tools.linear__list_connections.execute({}, {}); // => { connections: [ // { name: 'Work', accountLabel: 'Work' }, // { name: 'Personal', accountLabel: 'Personal' }, // ] } // Agent calls tools with the chosen connection. await tools.linear_get_issue.execute({ connection_name: 'Work', id: 'LIN-123' }, {});
String-array shorthand for
integrations: you can now pass a plain array of integration ids when no per-provider overrides are needed. The object form still works whenever you needallowTools,disallowTools,autoApproveTools,connectionId, ordisabled.// Shorthand connect({ integrations: ['linear', 'github'] }); // Object form (unchanged) connect({ integrations: { linear: { allowTools: ['linear_get_issue'] }, github: {}, }, });
disallowToolsper provider: each provider now accepts eitherallowToolsordisallowTools—ConnectIntegrationOptionsis a mutually exclusive union, so setting both is a compile-time and runtime error. UsedisallowToolswhen you want the whole toolset minus a few keys instead of an explicit allowlist.connect({ integrations: { // Everything except the delete tool linear: { disallowTools: ['linear_delete_issue'] }, // Explicit allowlist still works github: { allowTools: ['github_get_repo'] }, }, });
-
channels()now returns a live channel resolver instead of a fixed provider map. Channel connections created or removed on the Mastra platform are picked up by a running server automatically — no redeploy needed. (#25149)import { Mastra } from '@mastra/core/mastra'; import { channels } from '@mastra/connect'; export const mastra = new Mastra({ channels: await channels({ projectId: 'my-project' }), });
The resolver keeps one provider instance per integration, refreshes platform connections on a configurable
ttlMscache (30 seconds by default), and re-applies credentials when a connection changes. Slack credentials are fetched fresh from the platform before each app-management call, so tokens refreshed by the platform are always honored.If you previously awaited
channels()and read providers off the result as a plain object, call the resolver instead:const providers = await resolver().Requires
@mastra/core1.72.0 or later — the first release whoseMastraconstructor accepts aChannelsResolver(the peer dependency range has been raised to match). Older cores treat the resolver as a static provider record and fail at construction. -
Added 25 Microsoft Teams tools. Areas covered: (#25361)
- Teams — create, get, list joined teams, list members, add and remove members
- Channels — create, get, list, update, delete
- Channel messages — send, get, list, reply, list replies
- Channel tabs — create, list
- Chats — create, get, list, send messages, get message, list messages, list members
One
microsoft-teamsconnection powers both these tools and the Teams channel — no extra bot token or app registration to wire up.import { createMicrosoftTeamsTools } from '@mastra/connect'; const tools = createMicrosoftTeamsTools({ connectionId: 'conn_...' });
-
@mastra/slack,@mastra/telegram, and@mastra/discordare now direct dependencies of@mastra/connect. Installing@mastra/connectis enough to use any channel withchannels()— no separate channel package installs required. (#25125)
Patch Changes
-
Fixed Discord channel connections failing with
Discord rejected the bot token: 401: Unauthorized. The Discord bot token is now read from the connection's metadata (botToken, following Nango's Discord convention) instead of the OAuth credential — Discord's OAuth exchange only yields a user Bearer token, which can never authenticate as a bot. A Discord connection withoutbotTokenmetadata is skipped with a warning telling you to store the token on the connection. (#25260) -
Discord channel connections no longer require
applicationIdandpublicKeyon the platform connection. The bot token stored as the connection credential is enough — the provider resolves the rest from Discord automatically — so the "missing applicationId + publicKey" warning is gone. Connection metadata andproviderOptionsstill work as explicit overrides. (#25125) -
Telegram channels now run in delegated credential mode: the provider receives a platform-backed
tokenResolverinstead of a static bot token, so a token re-pasted or rotated on the platform takes effect on the very next Bot API call without a snapshot refresh, and the bot token is never persisted in provider storage. (#25272)import { channels } from '@mastra/connect'; // Telegram is wired automatically — the resolver reads the current // connection's api-key credential per Bot API call. No app-side config. const registrations = await channels({ projectId: process.env.MASTRA_PROJECT_ID! });
-
Fixed Slack channel connections that failed with "Slack refresh token is invalid" when connecting an agent. The Mastra platform now manages the Slack credential refresh cycle, so
channels()no longer competes with it over the single-use refresh token. (#25125)
@mastra/deployer@1.72.0
Patch Changes
- Fixed workspace packages listed as externals being bundled or missing from deployable output. They now remain runtime dependencies packaged with the build. Builds also resolve dependencies consistently from an app or monorepo root. (#25110)
@mastra/discord@1.2.0
Minor Changes
-
A bot token is now enough to configure
DiscordProvider—applicationIdandpublicKeyare resolved automatically from Discord'sGET /applications/@mewhen omitted, then persisted alongside the token. (#25125)// Before: all three credentials were required new DiscordProvider({ app: { botToken, publicKey, applicationId } }); // After: the bot token alone works new DiscordProvider({ app: { botToken } });
Explicitly supplied values (config,
DISCORD_PUBLIC_KEY/DISCORD_APPLICATION_IDenv vars, orconfigure()) still take precedence over the resolved ones.
Patch Changes
-
Switching the Discord bot token via
configure()now replaces the app config instead of merging into it. Previously the prior application'spublicKeyandapplicationIdsurvived the switch — including in persisted config — so the old application's Ed25519 key kept verifying inbound webhooks while the new bot token was active. The provider now drops everything derived from the old token and re-resolves the new application's identity fromGET /applications/@me. (#25149)For the same reason,
publicKeyandapplicationIdno longer fall back toDISCORD_PUBLIC_KEY/DISCORD_APPLICATION_IDwhen the bot token is supplied via config orconfigure()— stale environment values from a different application would otherwise attach to the new token. Environment fallback for those fields applies only when the bot token itself comes fromDISCORD_BOT_TOKEN.
@mastra/docker@0.9.1
Patch Changes
-
Fixed
wait()reporting a killed or timed-out process as a natural exit. Terminating a process returnedkill() === truewhile the resolvedCommandResultomittedkilled, so a forced termination was indistinguishable from a command that exited on its own. (#24775)Killing a process tears its own exec stream down, so Docker can deliver the stream's
endevent whilekill()is still confirming the process group is gone.endsettledwait()first — withoutkilled/timedOut— and the exit code it recorded then preventedclose, the only path that carried that metadata, from settling.end,close, anderrornow settle through a single path, andendwaits for an in-flight kill confirmation before settling. That wait is bounded, so a daemon that stops answering mid-kill cannot leavewait()pending; termination metadata is published by whichever event settles first. -
Failed
DockerTemplatebuilds no longer leave a stopped intermediate container behind. (#25262)
@mastra/duckdb@1.12.0
Patch Changes
-
Fixed
listTraces,listTracesLight, andlistBranchesscanning the entirespan_eventstable when a query matched nothing or requested a page past the end. These calls now return an empty page immediately, so empty filters and out-of-range pages stay fast and use little memory on large stores. (#25271) -
Reduced storage use for ended spans in
@mastra/duckdb. Span data returned by queries is unchanged. Fixes #25240. (#25268)
@mastra/e2b@0.12.2
Patch Changes
- Fixed code mode failing after an E2B sandbox auto-paused on timeout. Code mode now resumes the paused sandbox and runs the program, instead of failing until another command happened to wake the sandbox. (#25261)
@mastra/editor@0.15.4
Patch Changes
-
Fixed stored processor graphs dropping
tools,activeTools, andtoolChoiceafter parallel or conditional steps, which caused the next model call to run without the agent's tools. (#25063) -
Fixed
@mastra/editor/composiofailing to import on fresh installs by upgrading@composio/coreto ^0.18.0. Fixes #24512. (#25166)
@mastra/evals@1.10.4
Patch Changes
- Fixed trajectory scoring so blacklisted and repeated tool calls inside nested agent and workflow steps now lower the score. Previously only top-level calls were checked. Fixes #24925. (#24929)
@mastra/factory@0.18.0
Minor Changes
-
Added the
factory_review_sourcetool for Factory review-role sessions. It returns the Factory session URL that produced the review (the only field published on the PR/MR), the PR/MR author recorded at intake, the review card's own external source, and the intake-stamped repository identity. The review skills (factory-review,factory-rereview,factory-gitlab-review,factory-gitlab-rereview) now require calling this tool before publishing, cross-checkingtriggeredByandreviewTargetagainst the PR/MR fetched at Phase 1 (withboundRepositoryas the binding-side repository identity when the card carries no URL), and includingsessionUrlas aFactory Sessionblock in the published body. This makes misattributed reviews (e.g. a review that lands on the wrong PR, or approves and requests changes at once) traceable back to the exact session that produced them, and blocks a wrong-target review before it publishes. (#25013)The tool takes no arguments and is only registered in review-role sessions where
MASTRACODE_PUBLIC_URLis set to a non-blank browser-facing UI origin (a blank or whitespace-only value counts as unset, matching the Slack session-link surface). From an agent inside such a session:const { sessionUrl, triggeredBy, reviewTarget, boundRepository } = await tools.factory_review_source.execute({}); // sessionUrl: "https://factory.example.com/factories/<projectId>/workspaces/<sessionId>/threads/<threadId>" // triggeredBy: "octocat" | null // reviewTarget: { integrationId: "github", type: "pull-request", externalId: "github-pr:42", url: "https://github.com/acme/repo/pull/42" | null } // boundRepository: { provider: "github", repositoryId: 12345 } | { provider: "gitlab", host: "gitlab.example.com" | null, projectId: 101 } | null // Publish only `sessionUrl` on the PR/MR — `triggeredBy`, `reviewTarget`, and // `boundRepository` are inputs to the in-run cross-check and stay in the // session handoff so nothing on the review card's upstream (e.g. a Linear/Jira // issue slug) is leaked into a public review body: const publishedBlock = ['## Factory Session', `- Session: ${sessionUrl}`].join('\n');
The skills instruct the agent to end every published review body with a
## Factory Sessionsection carryingsessionUrlverbatim, so a suspicious review can be traced back to the session that produced it.
Patch Changes
-
Fixed Factory runs starting in the wrong repository when a project links several repositories. Automatic runs now stop when the target cannot be determined instead of choosing the first repository. (#25116)
-
Improved Factory default model changes to ask whether running work and review sessions should switch too. New runs always use the saved default; existing sessions keep their current model unless the user explicitly switches them, and switched sessions adopt the new model on their next step. (#25337)
-
Fixed Factory builds approved by someone other than the plan's owner starting in a new, empty session. The build now continues in the plan's session so it keeps the plan, and the planning agent stops once the build starts. Comments and edits on a GitHub issue no longer restart triage while its card is being built or reviewed (#25230). (#25270)
-
Prevent client writes to internal reconciliation metadata (#24993)
-
Added in-app GitHub and GitLab connections during Factory onboarding. Users can connect GitHub through the GitHub App install flow and connect GitLab without routing through Mastra Platform. The create-factory wizard now supports back navigation between steps. (#24855)
-
Transcript rows share one line style. Tool calls, reasoning, signals, notifications, skills and the "Thinking" indicator show an icon and a label, with a chevron only when there is more to show. A state signal names its state and shows its mode as a badge. A row whose message fits on its line no longer offers a disclosure that only repeats the line, and a tool call with nothing to show, including one that returned
nullor an empty result, has no disclosure at all. (#24694) -
Board transitions and linked work item cards now apply while agent runs occupy every dispatch slot. Previously, closing an issue or opening a PR while
MASTRACODE_DISPATCH_MAX_IN_FLIGHTruns were active left the card's stage stale until a run finished. Bookkeeping decisions now use their own small dispatch pool, so the board keeps mirroring GitHub while agents work. (#25289) -
Fixed Investigate on an Intake card not starting its triage session. The held triage run was marked finished without running because moving the card into Triage looked like the card had moved on from the run. (#25364)
-
Fixed Factory kickoffs being delivered several times when they arrived while the previous run was ending. A kickoff queued onto an ending run is now resent only if it never showed up in the thread, and it is dropped instead of resent once the card has moved to another stage or its role was handed over. This stops a finished phase from restarting and pushing extra commits. (#25277)
-
Review cards now show a merged pull request as merged as soon as the merge arrives, instead of offering Re-review until the next background sync. (#25369)
-
Stopped comments and edits on a closed issue from queuing a new triage run on its finished card. (#25370)
-
Pushes to a Factory-authored pull request now start a re-review automatically. Previously, once the review card reached Done, the re-review triggered by a new push waited for approval that was never requested, so someone had to click Re-review by hand. Pull requests from other authors still wait for approval as before. (#25371)
-
Pending tool approvals are restored from stored messages when a conversation reloads, so approval cards survive a server restart without replaying finished runs. (#24876)
-
Factory reviews now flag a visible misconfiguration warning when the review token is the PR author. Previously GitHub rejected the approve/request-changes submission and the verdict silently fell back to a plain PR comment. The verdict is still published as a PR comment (so the repair loop keeps working), with a warning placed after the verdict line explaining that a separate reviewer token is required for the verdict to count toward branch protection. (#25391)
-
Factory reviews no longer publish a verdict on a pull request head that has already moved. Before posting, the review and re-review skills check that the PR head still matches the commit they verified. If a push landed mid-review, they review the new commits before publishing. (#25139)
-
Fixed automated reviews failing with "Skill not found: factory-review". Review sessions started by the Factory now recognize their review role right away, so the review skills and the reviewer GitHub token are available from the first run. (#25341)
-
Fixed automated pull request reviews failing with "Skill not found: factory-review." when the review session was opened before its review role was assigned. The run now picks up the review skills at kickoff instead of retrying until it gives up. (#25341)
-
Fixed Factory cards looking finished while a review is still asking for changes. (#25377)
- Review cards: a review pass now records its verdict on the card and leaves it in Reviewing. The card shows "Changes requested" or "Approved" with the reviewed commit. A merged pull request moves a Review card to Done, a pull request closed without merging moves it to Canceled, and the next push starts a re-review automatically.
- Work cards: a Work card now moves from Building to Review when its pull request opens, shows the review verdict there, and moves to Done when the pull request merges. Agents can't move a Work card to Done while its pull request is still open or while the review requests changes.
-
Fixed subagents in Factory sessions ignoring the project's default model. Explore, plan, and execute subagents now use the Factory default model instead of per-subagent models from the server's settings, which could name providers the Factory has no credentials for. (#25460)
In Slack threads, subagents follow the sender's active model pack (explore uses the pack's fast model, plan uses plan, execute uses build), matching the main agent. When the sender has no pack they use the Factory default.
-
Fixed Factory work sessions being able to load the review skills and approve their own pull requests. The factory-review, factory-rereview, factory-gitlab-review, and factory-gitlab-rereview skills are now only available to sessions running a Review phase (#25228). (#25273)
-
Fixed Factory colors that lost contrast after the color role update: the sidebar Beta badge is brand green again and no longer clipped, invalid work item fields show a red border, project nodes in the knowledge graph have a visible ring, and the overview funnel shows people in pale purple again. (#25484)
-
Fixed missing Slack feedback when Factory message preparation fails before dispatch. (#25362)
-
New GitHub issues now create Factory work items when events are polled. Open issues filed after a repository was linked are recovered by the issue reconcile sweep when they have no work item. Changes to existing issues re-evaluate their linked work items. Events skipped during polling are logged at debug level. (#25201)
-
Fixed session resume silently provisioning a replacement VM instead of reattaching to the original sandbox. Factory now persists the provider's physical sandbox id and forwards it back on resume, so providers that reattach by id (like Railway) reconnect to the same VM instead of orphaning it and doubling compute cost. Refs #23974. (#24004)
-
Fixed pull requests opened through Factory so connected GitHub users are assigned to them, making their pull requests easier to find by assignee. (#25117)
-
Blocked automatic Planning-to-Building transitions unless the project enables auto-approval or a person approves the plan. (#25329)
-
Fixed new Slack threads starting chat-only sessions when they cannot be backed by a repository. (#24952)
- Explain in Slack why a new Factory session cannot start when the linked project has no repository or source-control connection.
- Keep account-link and project-selection prompts for senders who cannot yet be routed.
- Keep chat-only sessions for deployments without account linking, projects, or source-control integration.
- Leave existing Slack conversations unchanged.
-
Fixed Factory sign-in on custom domains to explain that Mastra Platform authentication requires a Mastra-hosted domain and point to supported custom auth providers. (#25326)
-
Fixed interrupted worker messages reporting successful delivery and recording a success audit. (#24321)
-
Fixed the Label filter on the Factory Review and Work boards showing no values. Pull request, merge request, and Linear issue cards now carry their labels, so the boards can offer and filter by them. (#25095)
-
Fixed Slack sessions using observational-memory models from an incompatible provider. New and restarted sessions now use a memory model compatible with their running model, including after a model switch fails. (#25412)
-
Fixed replies from another linked Slack user using the responder's model credentials. Existing Factory Slack threads now keep using the session owner's provider credentials while preserving the responder's message attribution. Responders must belong to the session owner's organization, and subscribed follow-ups are rejected when the existing internal thread or owning session cannot be found. (#25474)
-
Slack setup prompts now tell people to mention the bot again, or message it again in direct messages, after connecting their account or picking a default factory. (#25323)
-
Added incident.io intake for self-managed servers: the generated Factory Server now wires the incident.io integration from INCIDENT_IO_API_KEY, and the incident.io status route reports the credential mode so clients can tell a deployment API key from Platform-managed connections. (#25178)
import { MastraFactory } from '@mastra/factory'; import { IncidentioIntegration } from '@mastra/factory/integrations/incidentio/integration'; const factory = new MastraFactory({ // ... integrations: [new IncidentioIntegration({ apiKey: process.env.INCIDENT_IO_API_KEY! })], });
-
Fixed Slack default factory prompts so they appear as visible thread replies and notify the sender. (#25323)
@mastra/fastify@1.5.16
Patch Changes
- Fixed
@mastra/fastifydropping response headers on streamed (datastream-response) routes. Auth cookies (Set-Cookie), redirects (Location),Content-Type, and custom headers now reach the client, so SSO login/callback, sign-in, sign-up, refresh, and logout work on Fastify. Multiple cookies are sent as separate headers, and headers set by plugins such as CORS are preserved. (#25280)
@mastra/inngest@1.10.1
Patch Changes
-
Fixed
streamUntilIdle()andresumeStreamUntilIdle()oncreateInngestAgent()agents running in the Mastra server process instead of on Inngest. The/stream-until-idleand/resume-stream-until-idleroutes now run durably, the same asstream(messages, { untilIdle: true }). Fixes #25159. (#25190) -
Fixed recovery requests for agents created with
createInngestAgent().POST /api/agents/:agentId/recovernow returns a clear 400 "not supported" error that points toobserve(runId)for reconnecting to a running stream, instead of a 500. Withrecovery.durableAgents: 'auto', startup recovery skips Inngest agents instead of logging an error for each one. Fixes #25160. (#25186) -
Fixed Inngest durable agents reporting
[object Object]when a run fails. The stream error,generate()rejection, HTTP 500 response, and workflow tracing span now carry the real failure message, such as Inngest'sstep output size is greater than the limiterror. Fixes #25161. (#25182) -
Fixed
InngestAgent.resume()accepting runs that had already finished. Resuming a completed run now fails with a "not suspended" error instead of running the previously suspended tool again, so a declined tool approval can no longer be approved after the run ends. (#25181) -
Pass prior agent steps to per-step processor hooks in Inngest workflows. (#25217)
-
Fixed resuming the second of several tool approvals from one agent turn by its
toolCallIdon Inngest durable agents. Approving the next pending tool call right after its approval request arrived used to fail withno suspended tool call with id. The resume now waits briefly for the new approval to be saved. Fixes #25158. (#25173) -
Inngest durable agents now tell Mastra which workflow name their runs are stored under, so pending tool approvals can be listed, approved, and declined over the HTTP API. Fixes #25154. (#25167)
-
Fixed stopping an Inngest agent by thread or run id. Calling
abortThreadStream()orabortRunStream()(includingPOST /api/agents/:agentId/threads/abortand the Studio stop button) now stops the run on the Inngest worker, and the stream ends withfinishReason: 'abort'. Previously the call reported success while the run and its tools kept going. Fixes #25156. (#25192) -
Fixed Inngest durable agents ignoring
structuredOutput.generate()now returns the parsedobjectinstead of only the JSONtext, andstream(),resume(),resumeGenerate(), andobserve()in the same process also expose it. Fixes #25148. (#25170)
@mastra/langfuse@1.5.11
Patch Changes
-
Use the GenAI semantic convention for reasoning output tokens so Langfuse does not double-count them with total output tokens. (#24939)
-
Fixed application version mapping on Langfuse traces and observations, and allowed overriding OpenTelemetry resource attributes. For example, with Langfuse credentials set in the environment: (#25101)
import { LangfuseExporter } from '@mastra/langfuse'; const exporter = new LangfuseExporter({ resourceAttributes: { 'service.version': '2.3.1' }, });
@mastra/libsql@1.24.0
Patch Changes
-
Fixed
$orand$normetadata filters treating the keys of one condition as alternatives. A filter like{ $or: [{ category: 'electronics', inStock: true }, { name: 'novel' }] }also matched out-of-stock electronics, anddeleteVectorswith such a filter deleted more vectors than intended. The keys of each condition are now combined with AND. (#25353)Also in: @mastra/pg@1.28.0
-
Fixed schedules failing with "no such column: owner_type" on databases created by older versions. Existing schedules and trigger history are kept, and the upgrade is safe when several processes share the same database. (#25475)
-
Workflow run lists requested with
summary: truenow read only status and timestamp from each snapshot, so large snapshots are not transferred from the database. (#25135)Also in: @mastra/pg@1.28.0
@mastra/livekit@0.4.1
Patch Changes
- Fixed workflow-driven voice turns being reported as successful when the workflow run failed.
createWorkflowReplyGeneratornow errors the reply stream when the run fails and no longer callsonTurnCompletefor it, so a failed turn is no longer saved to memory as an empty or partial assistant reply. (#25068)
@mastra/loggers@1.3.3
Patch Changes
-
Fixed
HttpTransportignoring explicit0andfalseretry options. SettingretryOptions: { maxRetries: 0 }now makes a single request,retryDelay: 0retries immediately, andexponentialBackoff: falseuses a constant delay between retries. (#25265) -
Fixed the Upstash logger so
maxListLengthactually trims the log list. Trim arguments were previously stored as log entries, causing unbounded list growth and unreadable entries inlistLogs(). (#25496)
@mastra/mcp@2.1.1
Patch Changes
- Fixed MCP tool calls failing with a schema validation error when tools are defined with zod v3 schemas.
MCPClientalso now accepts tools from other MCP servers that describe their inputs with JSON Schema draft 2019-09. (#25290)
@mastra/memory@1.33.0
Minor Changes
-
You can now control observational memory retries and choose whether a failed Observer or Reflector stops the agent turn. (#24863)
maxRetriessets how many times a failed Observer or Reflector call is retried. The default is8.failurePolicy: 'continue'lets the agent turn finish when observation or reflection still fails after all retries. The default,'abort', keeps the current behavior.- Messages that were not observed are retried on a later turn.
- A cancelled turn always stops, whatever the policy.
- Structured extractors now retry a temporary provider failure once. This retry does not use
maxRetries, and a failed extraction still does not block the turn.
import { Memory } from '@mastra/memory'; const memory = new Memory({ options: { observationalMemory: { observation: { maxRetries: 2, failurePolicy: 'continue' }, reflection: { maxRetries: 2, failurePolicy: 'continue' }, }, }, });
Patch Changes
-
Fixed Anthropic extended-thinking threads with working memory getting stuck on every turn with "thinking blocks in the latest assistant message cannot be modified". Saving messages no longer keeps a thinking-only step behind after hiding its
updateWorkingMemorycall, and the hidden call no longer reappears in later prompts. Fixes #22798. (#24924) -
Fixed observational memory merging a later observation group into an earlier truncated group when the truncated group quoted an
<observation-group>tag inline. The later group is now parsed and shown in reflections with its own ID and range, and stripping group tags keeps both the quoted text and the later observations. (#24927) -
Fixed Observational Memory leaving many remembered dates without a relative time such as "3 weeks ago". Dates with a time ("Mar 22, 2025 at 18:08"), ranges written with an en dash ("Aug 13–27, 2024"), month or year dates ("August 2024", "late 2023", "2035"), and date-range headers the reflector writes ("Date: Aug 1, 2024 - Feb 28, 2025") are now annotated. Dates written inside observations that include their year, such as "exam on January 10, 2024", are annotated too. Dates without a year are left unchanged. (#25179)
-
Observational memory failure messages now include the Mastra error ID (for example
MASTRA_STORAGE_LIBSQL_UPDATE_BUFFERED_OBSERVATIONS_FAILED), so storage failures show which operation failed instead of only the raw driver message. (#25366) -
Deprecated Observational Memory
scope: 'resource'. Resource scope works much worse than thread scope for prompt caching and for the agent's understanding of the conversation. Using it now logs a one-time warning, and the option is marked@deprecatedin types and docs. A new knowledge and subconscious memory primitive for cross-thread memory is coming soon and will replace resource scope. Until then, removescopeto use the default thread scope and enableretrievalor resource-scoped working memory for cross-thread continuity. (#24933)// Before observationalMemory: { model: 'google/gemini-2.5-flash', scope: 'resource' } // After observationalMemory: { model: 'google/gemini-2.5-flash', retrieval: true }
-
Fixed Observational Memory relative dates being off by a day when memory is read on a server in a different time zone from the one that wrote it. For example, an event from 8 days ago showed as "7 days ago". Fixed "1 week later" showing as "6 days later" across a daylight-saving change. A planned action dated for the current day is no longer marked as likely already happened. (#25177)
-
Fixed
getSystemMessage()returning working memory instructions that contained the word "null" when no working memory had been saved yet. It now shows "No working memory data available." instead. Fixes #23724. (#25057)
@mastra/modal@0.7.0
Minor Changes
-
Added Modal Volume mounts and a volume-backed workspace filesystem. (#25072)
- New
volumesoption onModalSandboxmounts Modal Volumes at the given paths. - New
sandbox.reloadVolumes()picks up writes made to a Volume by other sandboxes. - New
ModalFilesystemexposes a path inside the sandbox (such as a Volume mount) to workspace file tools, so agent files persist across sandbox restarts.
const sandbox = new ModalSandbox({ workingDirectory: '/workspace', volumes: { '/mnt/agent': volume } }); const workspace = new Workspace({ sandbox, filesystem: new ModalFilesystem({ sandbox, basePath: '/mnt/agent' }), });
- New
@mastra/mongodb@1.20.0
Patch Changes
- Fixed MongoDB skills listing ignoring the
statusandentityIdsfilters.list({ status: 'published' })no longer returns draft skills,list({ entityIds: [] })now returns no skills, and pagination totals reflect the filtered results. (#25133)
@mastra/mysql@0.11.0
Patch Changes
- Fixed MySQL skills storage dropping a skill's inline file tree (
files) on save, so skill files now persist and load correctly. (#25267)
@mastra/nestjs@0.2.31
Patch Changes
-
Added support for NestJS v12.
@mastra/nestjsnow accepts@nestjs/commonand@nestjs/corev12 as peer dependencies, so installing it in a NestJS v12 app no longer fails with a peer dependency conflict. Fixes #25077. (#25408) -
Fixed
MastraAuthGuardfailing to resolve when used on your own controllers.MastraModule.register()andregisterAsync()now exportAuthService, so you can protect app routes with Mastra's auth: (#25233)@Controller('api/things') @UseGuards(MastraAuthGuard) export class ThingsController {}
@mastra/observability@1.18.2
Patch Changes
- Fixed
model_stepspans exporting the provider's raw HTTP response body in their metadata. For some providers (for example, Azure OpenAI's Responses API) this added hundreds of kilobytes to each step span while repeating output and usage already on the span. Response headers are still exported, so provider request IDs and rate-limit details remain available for debugging. Fixes #24826. (#25347)
@mastra/pg@1.28.0
Patch Changes
-
Fixed duplicate observational memory records in PostgreSQL when several agents, Memory instances, or server processes start or reflect on the same thread or resource at the same time (#22188). Only one record is now created per generation, and every caller gets that same record back. This works behind transaction-mode connection poolers such as PgBouncer and Supabase. (#24923)
No schema change or migration is needed. Databases that already contain duplicate records keep working: Mastra now always reads the same one (the earliest created) instead of switching between them.
-
Fixed PgVector
$orand$norfilters dropping field keys that sit next to a nested logical operator. In{ $or: [{ $and: [{ a: 1 }], c: 2 }, { d: 3 }] }theccondition was ignored. It is now combined with the nested$and. (#25353)
@mastra/platform-workspace@1.6.2
Patch Changes
- Fixed nested folders in
PlatformFilesystemworkspaces so they open in Studio. Folders created by writing a file under them are now reported bystatandexists, and can be listed withreaddir. (#25184)
@mastra/playground-ui@60.0.0
Minor Changes
-
Added three formatters so apps stop hand-rolling them: (#25286)
formatBytesfrom@mastra/playground-ui/utils/number:formatBytes(1536)returns1.5 KB.formatPercentfrom@mastra/playground-ui/utils/number:formatPercent(0.42)returns42%, tiny ratios show<0.1%, and{ signed: true }returns+12.3%for changes.pluralizefrom@mastra/playground-ui/utils/string:pluralize(3, 'entry', 'entries')returns3 entries.
-
ScrollAreafade depth can now be set per side throughmask. Fades stay 2rem deep by default; the main sidebar uses a 3rem top and 5rem bottom fade. A fade now grows with how far the content is scrolled from that edge, up to its depth, instead of appearing at full depth as soon as the content scrolls. (#25415)<ScrollArea mask={{ top: '3rem', bottom: '5rem' }}>{items}</ScrollArea>
Breaking: Removed the
viewPortClassNameandviewportRefprops fromScrollArea. To style or reach the scrolling viewport, render the newScrollAreaViewportas the direct child ofScrollAreaand pass itclassNameorref. When you do not render it,ScrollAreaadds it for you.// Before <ScrollArea maxHeight="20rem" viewPortClassName="px-4" viewportRef={scrollRef}> {items} </ScrollArea> // After <ScrollArea maxHeight="20rem"> <ScrollAreaViewport ref={scrollRef} className="px-4"> {items} </ScrollAreaViewport> </ScrollArea>
Breaking: Removed the deprecated
showMaskprop fromScrollArea. Usemaskinstead. -
Dialognow has one modern shell. Every dialog gets the same padding, spacing, close button, scroll fades and motion, so call sites only pass layout. (#25432)- Sizes:
<DialogContent size="sm" | "md" | "lg" | "xl" | "full">sets the width: 24rem, 32rem (the default), 42rem, 56rem, or the full viewport. Height grows with the content up to a viewport cap. - Body:
DialogBodyis a padded ScrollArea whose edges fade while content scrolls.layout="fill"makes the body take the remaining height and lets its children handle scrolling, for example split panes or a pinned search.flushremoves the inset. - Footer actions:
DialogCancelcloses the dialog.DialogActionis the primary action:onConfirmfor a click,confirmation="hold"for press-and-hold, and nowtype="submit"to submit the surrounding form. Both are disabled while the dialog ispending. - Root:
intent="destructive"now works on every dialog: it sets thealertdialogrole, focuses Close first, and ignores outside clicks. A<form>placed directly insideDialogContentfits the layout without extra classes. - Text: titles use the
headingrole (16px), and descriptions use thebodyrole, muted.DialogDescriptionis now visible. - Motion: dialogs scale up from 96% with a strong ease-out curve and close faster than they open. With reduced motion, they only fade.
- AlertDialog: now built from the same shell and parts as
Dialog, so it has the same padding, text roles, footer buttons and motion. Its header, body, footer, title, description and cancel are theDialogparts.AlertDialog.Contenttakes the samesizeprop and defaults tosm.AlertDialog.Bodyis now the padded scroll area.
// Before <Dialog open={open} onOpenChange={isSaving ? undefined : onOpenChange}> <DialogContent className="max-w-2xl"> <DialogHeader className="border-b border-border px-4 py-4"> <DialogTitle>Import items</DialogTitle> <DialogDescription className="not-sr-only text-caption">Paste JSON or upload a file.</DialogDescription> </DialogHeader> <DialogBody className="max-h-[70vh] overflow-y-auto px-4 py-5">…</DialogBody> <DialogFooter className="px-4 pt-4"> <Button icon={<X />} onClick={() => onOpenChange(false)}>Cancel</Button> <Button variant="primary" onClick={handleImport}>Import</Button> </DialogFooter> </DialogContent> </Dialog> // After <Dialog open={open} onOpenChange={onOpenChange} pending={isSaving}> <DialogContent size="lg"> <DialogHeader> <DialogTitle>Import items</DialogTitle> <DialogDescription>Paste JSON or upload a file.</DialogDescription> </DialogHeader> <DialogBody>…</DialogBody> <DialogFooter> <DialogCancel>Cancel</DialogCancel> <DialogAction onConfirm={handleImport}>Import</DialogAction> </DialogFooter> </DialogContent> </Dialog>
Removed
- The
variantprop onDialogand theDialogVarianttype. Every dialog now uses the modern shell. asChildonDialogTrigger,DialogCloseandAlertDialog.Trigger. Userenderinstead:<DialogTrigger render={<Button>Open</Button>} />.AlertDialog.PortalandAlertDialog.Overlay.AlertDialog.Contentrenders both.- The
@mastra/playground-ui/lib/as-childhelper. - The
dialog-overlay-animanddialog-content-animclasses. To find the overlay in a test, query[data-slot="dialog-overlay"].
- Sizes:
-
Added
FileDropBackdrop, which wraps any field and shows a full-page "Drop to upload" empty state (customizable withlabelanddescription) while a file is dragged over the window. Dropped files matching the optionalacceptfilter are passed toonFilesDrop. (#25299)import { FileDropBackdrop } from '@mastra/playground-ui/components/FileDropBackdrop'; <FileDropBackdrop accept="image/*,.pdf" onFilesDrop={files => setAttachments(prev => [...prev, ...files])}> <Composer>…</Composer> </FileDropBackdrop>;
-
Added
PageHeader.Eyebrowfor a back link above the page title, and fixedPageHeader.Meta besidesitting below the title text when the header has a tall icon. (#25155)PageHeader.Icon,PageHeader.Action, andPageHeader.Meta besidenow center on the first line of the title, including when the title wraps. A header with an icon or action contains them, so they no longer overflow the header, push the title up, or sit below it. Beside meta no longer shrinks when the title is long.<PageHeader> <PageHeader.Eyebrow> <Link to="/alerts"> <ArrowLeftIcon aria-hidden /> Back to alerts </Link> </PageHeader.Eyebrow> <PageHeader.Title>Create alert</PageHeader.Title> </PageHeader>
Breaking: removed the
title,description,icon, andisLoadingprops fromPageHeader. Compose the slots instead:// Before <PageHeader title="Agents" description="Build and test agents." icon={<BotIcon />} /> // After <PageHeader> <PageHeader.Icon> <BotIcon /> </PageHeader.Icon> <PageHeader.Title>Agents</PageHeader.Title> <PageHeader.Description>Build and test agents.</PageHeader.Description> </PageHeader>
For a loading state, pass
isLoadingtoPageHeader.TitleandPageHeader.Description. -
Keyboard focus now looks the same everywhere: a 1px neutral outline with no green halo. Any focusable element that does not style its own focus gets it by default.
focusRingis a single class string instead of an object, withfocusRingInsetfor clipped full-width rows andfocusRingOffsetfor checkboxes, radios, switches, and filledprimaryanddestructivebuttons, where a line flush with the fill would disappear. Fields, buttons, segmented buttons, and clickable cards show focus by repainting their own rim to the same colour, so they keep their elevation; their focus edge is stronger than before, at the--border-focusweight tuned for 3:1 contrast. The halo,ringalias, and lighter rim focus tokens are removed. The Trace Intelligence empty state also drops its glowing and pulsing decorations. (#25441)Before:
<button className={cn('rounded-md', focusRing.visible)} /> <div className="focus-visible:ring-1 focus-visible:ring-ring focus-visible:shadow-focus-ring" />
After:
<button className={cn('rounded-md', focusRing)} /> <div className={focusRingInset} />
Removed Use instead focusRing.visible,.default,.simplefocusRingFocusRingStyletypenone --ring,ring-ring,Colors.ring--border-focus,ring-border-focus--shadow-focus-ring,shadow-focus-ringnone --focus-halo,Shadows['focus-ring']none --surface-rim-focus,--field-rim-focus-on-surface--border-focus -
Each thing an agent does in a chat now renders as its own
Activityline: every tool call, reasoning step, signal, notification, skill and plain "working" row. A body is optional. Without one, the line has no chevron and otherwise looks the same, so a step that returns nothing reads like one that does. (#24694)import { ActivityItem } from '@mastra/playground-ui/components/ai/activity'; import { ToolCallOutput } from '@mastra/playground-ui/components/ai/tool-call'; <ActivityItem icon={<Sparkles aria-hidden />} label="Thinking" status="running" aria-label="Thinking" />; <ActivityItem icon={<FileText aria-hidden />} label="Read file" detail="src/agent.ts" aria-label="Tool: view"> <ToolCallOutput text={output} /> </ActivityItem>;
The
ToolCallshell is renamed toActivityThe compound parts moved to
components/ai/activityunder new names:ToolCall*becomesActivity*,ToolCallPresentedHeaderbecomesActivityHeadline, andToolCallStatusbecomesActivityStatus.ActivityHeadlinetakes its icon as an element instead of a component, and keeps thedescriptionprop that shows a command's description in place of its label and detail. Itsdisclosureprop is removed: passfoldable={false}toActivityinstead. The tool-specific blocks stay incomponents/ai/tool-call:ToolCallArguments,ToolCallOutput,ToolCallCommand,ToolCallGroupandpresentTool.// Before import { ToolCall, ToolCallTrigger, ToolCallPresentedHeader, ToolCallContent, } from '@mastra/playground-ui/components/ai/tool-call'; <ToolCall status={status}> <ToolCallTrigger> <ToolCallPresentedHeader icon={Search} label={label} detail={detail} /> </ToolCallTrigger> <ToolCallContent>{body}</ToolCallContent> </ToolCall>; // After import { Activity, ActivityTrigger, ActivityHeadline, ActivityContent, } from '@mastra/playground-ui/components/ai/activity'; <Activity status={status}> <ActivityTrigger> <ActivityHeadline icon={<Search aria-hidden />} label={label} detail={detail} /> </ActivityTrigger> <ActivityContent>{body}</ActivityContent> </Activity>;
The screen-reader status text is now "Running" or "Failed" instead of "Tool call running" or "Tool call failed", because the line is no longer only for tools.
Signals, notifications and reasoning are
Activitypresetscomponents/ai/chat-eventis removed. Its presets moved intocomponents/ai/activityand are named after the line they draw:ChatSignalis nowSignalActivityandChatNotificationis nowNotificationActivity. Each one picks the icon, badges and body for one kind of event overActivityItem.The card presentation of signals and the notice presentation of notifications are removed along with their
variantprop andgetNotificationNoticeVariant. A notification's priority is now a coloured badge on the line: urgent is red, high is orange, medium is blue. Its status and pending count are badges beside it. A system reminder names its path as the detail of the line.// Before import { ChatNotification } from '@mastra/playground-ui/components/ai/chat-event'; <ChatNotification variant="notice" label="github / issue-opened" message={message} priority="high" />; // After import { NotificationActivity } from '@mastra/playground-ui/components/ai/activity'; <NotificationActivity label="github / issue-opened" message={message} priority="high" />;
Reasoningmoved out ofdomains/chat/messages/reasoningand joins them asReasoningActivity, with the same props.hasVisibleReasoninganswers whether it would render anything, so a transcript can skip an empty reasoning part without drawing it.ReasoningStreamingLineis removed:ReasoningActivitycovers the waiting state itself, and while it streams with no text yet, it shows a busy "Reasoning" line with no disclosure.// Before import { Reasoning } from '@mastra/playground-ui/domains/chat/messages/reasoning'; <Reasoning text={text} streaming />; // After import { ReasoningActivity } from '@mastra/playground-ui/components/ai/activity'; <ReasoningActivity text={text} streaming />;
A line only folds when its body says more than the line
A short single-line message fits in the preview, so opening a disclosure used to reveal a copy of the line above it. Such a line now has no disclosure and wraps its detail instead of clipping it, so a narrow transcript never hides the end of a sentence it offers no way to open. Because folding is now the exception, a line that folds shows a dimmed chevron at rest instead of only on hover.
Notification badges wrap in narrow transcripts without squeezing the message out. Linked notifications keep their full message in the expanded body. Expanded messages preserve line breaks and wrap long URLs. A notification link is now announced by its visible text followed by the message preview, for example "Open on GitHub: The pull request was merged…", so voice control can open it by what it says.
The same rule covers a composed
Activity: passfoldable={false}when there is nothing to open, and the line drops its disclosure button and its empty body. A tool call with no arguments, no output and no result is one example, and so is a call whose arguments are an empty object.hasToolArgumentstells you whetherToolCallArgumentswould render anything, andawaitsToolApprovaltells you whetherToolApprovalButtonswould.import { Activity, ActivityContent, ActivityHeadline, ActivityTrigger, } from '@mastra/playground-ui/components/ai/activity'; import { hasToolArguments, ToolCallArguments, ToolCallOutput } from '@mastra/playground-ui/components/ai/tool-call'; const foldable = hasToolArguments({ toolName, args }) || output !== undefined; <Activity foldable={foldable} status={status}> <ActivityTrigger> <ActivityHeadline icon={<Search aria-hidden />} label={label} detail={detail} /> </ActivityTrigger> <ActivityContent> <ToolCallArguments toolName={toolName} args={args} /> {output !== undefined && <ToolCallOutput text={output} />} </ActivityContent> </Activity>;
A line that gains a body as its arguments stream in keeps its headline mounted: its shimmer does not restart, its detail does not fade in again, and the chevron fades into a slot that was already reserved, so the text does not shift sideways.
Because the disclosure button now lies over the whole line, put anything that needs its own hover, such as a timestamp with a
title, inActivityLeading. It stays reachable above the button, so the tooltip still shows, and a click on it still opens the line.ActivityHeadlineandToolCallGroupalready wrap theirleadingcontent in it.ActivityTriggeris now a wrapper around that button rather than the button itself, so props such asonClickordisabledno longer reach it: drive the line throughopen,onOpenChangeandfoldableonActivity.ChatTimeGapis replaced byTranscriptDividerThe transcript separator is a
role="separator"rule, not an event, and it no longer parses a time string. It takes the label and, separately, the timestamp that belongs intitle, and renders nothing when the label is empty.// Before import { ChatTimeGap } from '@mastra/playground-ui/components/ai/chat-event'; <ChatTimeGap text="24 minutes later — Sep 17, 2026, 2:24 PM" />; // After import { TranscriptDivider } from '@mastra/playground-ui/components/ai/transcript-divider'; <TranscriptDivider label="24 minutes later" title="Sep 17, 2026, 2:24 PM" />;
ChatSkillis removed: the Factory was its only consumer, so it now composesActivityItemitself.SignalActivityandNotificationActivityno longer set their own width or vertical margin, so the caller places them.Studio draws workspace and memory steps as
Activitylines tooListing files, running a sandbox command and observational memory used to render their own cards, so a group of tool calls mixed two styles. They now use the same line as every other tool call. A listing shows its path on the line, and its summary and filesystem link beside it. A sandbox command shows its command on the line, and its sandbox link, exit status and duration beside it. The sandbox link is no longer nested inside the toggle button. A sandbox command now starts folded like any other call, unless it waits for approval. An observation or reflection shows its token counts on the line and keeps its observations, current task, suggested response and extractions in the body. A failed one is a failed line.
-
Added a
switcherslot toCrumbfor entity switchers. On the current crumb, clicking anywhere on the crumb (name or chevron) now opens the switcher, instead of only the small chevron. On earlier crumbs the name still navigates back and the chevron opens the switcher. While t