Highlights
Opt-in Storage Retention + storage.prune() (Core + Postgres/libSQL/MongoDB)
Mastra now supports per-domain/per-table retention policies (retention: { ...maxAge }) and a safe, batched, resumable storage.prune() to delete aged data across major “growth” tables (memory, observability spans, workflow snapshots, background tasks, experiments, schedules, etc.). Postgres/libSQL/MongoDB adapters add first-class pruning implementations (including partition/chunk drops for v-next observability in Postgres).
Multi-tenant Isolation for Datasets, Experiments, Scores, and Scorers
End-to-end optional organizationId/projectId scoping was added across dataset and experiment reads/deletes (including storage-layer predicates to prevent cross-tenant access), plus tenancy metadata for persisted scores and stored scorer definitions with scoped list APIs. Server routes and client-js methods also accept tenancy parameters so HTTP and SDK usage can enforce the same isolation.
Persisted Trace Scoring APIs (scoreTrace, scoreTraceBatch) + Score Provenance
New scoreTrace() and scoreTraceBatch() let you score already-stored traces without re-running agents, using an existing scorer instance and bounded concurrency for batches. Persisted scores now optionally include batchId, datasetId, and datasetItemId so baseline scoring passes can be grouped and joined back to dataset items across supported stores.
Agent Authoring & Runtime Improvements (Nested Subagents, Durable Parity, Model Router Safety)
File-based agents can now nest subagents/ up to three levels deep, enabling deeper delegation hierarchies without custom wiring. Durable agents gained significant behavior/streaming parity fixes (signals draining/echo, stop semantics, processor/tool lifecycle hooks, header sanitization, replay ordering), and the model router now strips unsupported sampling params (e.g., temperature) to prevent 400s on models that don’t accept them.
New Workspace/Sandbox Options and Providers (Apple Container, Mesa, Railway, E2B)
New @mastra/apple-container adds an Apple container CLI workspace sandbox provider, and @mastra/mesa adds a Mesa filesystem provider for workspaces. RailwaySandbox gains checkpoint-backed restart/reconnect, and E2BSandbox adds a forwarded network option for finer-grained network controls and per-host request transforms.
Breaking Changes
@mastra/playground-uiremoved root named exports; import from explicit subpaths (e.g.@mastra/playground-ui/components/Button).@mastra/playground-uiremovedSearchbar; useInputGroupcomposition instead.
Changelog
@mastra/core@1.49.0
Minor Changes
-
Added opt-in storage retention. Declare per-table
maxAgepolicies in theretentionconfig, then callstorage.prune()to delete rows older than their age. Anything you don't configure is kept forever, so there is no change until you opt in. (#18733)Retention covers growth tables across ten domains —
memory(threads, messages, resources),threadState,observability(spans),scores,workflows(run snapshots),backgroundTasks,experiments,notifications,harness(sessions), andschedules(fire history). Anchors are chosen somaxAgeis honest: creation time for append-only logs, last activity for workflow snapshots and thread state, and completion time for background tasks and experiments (in-flight work is never pruned). User-authored artifacts and config (agents, skills, workspaces, datasets, schedule definitions, and so on) are not prunable.prune()is safe on large tables: it deletes in bounded, batched, resumable, cancellable chunks and never locks the database for long. Call it from your own scheduler; when a result reportsdone: false, eligible rows remain and the next run continues.prune()only deletes rows — reclaiming disk to the OS is left to the underlying database and the operator.const storage = new MastraCompositeStore({ id: 'composite', retention: { memory: { messages: { maxAge: '30d' }, threads: { maxAge: '90d' } }, observability: { spans: { maxAge: '7d' } }, }, domains: { /* ... */ }, }); // Wire this to your own cron — Mastra never runs it for you. const results = await storage.prune();
-
Added maxDurationMs, maxWidth, and maxHeight options to BrowserRecordingOptions. These can now be set on the recording config object to provide defaults for every recording, instead of relying on agent instructions to pass them to the tool at start time. (#18814)
const browser = new AgentBrowser({ recording: { outputDir: './recordings', maxDurationMs: 60_000, maxWidth: 1280, maxHeight: 720, }, });
Per-recording overrides via the browser_record tool still take precedence.
-
File-based agents can now nest subagents up to three levels deep. A subagent directory can declare its own
subagents/, and each level is assembled and wired into its parent as a delegation tool. Levels deeper than the cap are ignored with a warning. (#18780)src/mastra/agents/ supervisor/ # depth 0 subagents/ researcher/ # depth 1 subagents/ summarizer/ # depth 2 -
Fixed a cross-tenant data-access issue on datasets by scoping
DatasetsManager.getandDatasetsManager.deleteto tenancy filters. (#18750)Previously
get({ id })anddelete({ id })looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of whichorganizationId/projectIdit belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).What changed
DatasetsManager.getandDatasetsManager.deleteaccept optionalorganizationIdandprojectId.- The tenancy is stashed on the returned
Datasethandle and forwarded to every downstream storage call (getDetails,update,addItem, item batch ops,startExperimentAsync). - The abstract storage contract (
getDatasetById,deleteDataset) gained an optionalfilters?: DatasetTenancyFiltersarg. - Item-mutation inputs (
AddDatasetItemInput,UpdateDatasetItemInput,BatchInsertItemsInput,BatchDeleteItemsInput) andUpdateDatasetInputaccept optionalfiltersfor the internal existence check.
Behavior
- Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
- On tenancy mismatch,
getthrows NOT_FOUND (returns null at the storage layer) anddeleteis a silent no-op — matching how a missing id already behaves, so existence does not leak through error timing or messages.
Example
// Before const ds = await mastra.datasets.get({ id }); await mastra.datasets.delete({ id }); // After — scope to a tenant const ds = await mastra.datasets.get({ id, organizationId, projectId }); await mastra.datasets.delete({ id, organizationId, projectId });
-
Added
scoreTrace()andscoreTraceBatch()to@mastra/core/evals/scoreTracesfor scoring stored traces without re-running the agent. (#18331)scoreTrace()can score either a stored trace reference or a preloadedTraceRecord, and it returns the persistedScoreRowDataafter the write.scoreTraceBatch()runs one scorer instance across multiple stored traces with bounded concurrency and returns per-target success and failure results.
Why
This gives baseline-style callers a small public API for persisted trace scoring when they already have a scorer instance, without widening the existing workflow-based
scoreTraces()API.Before
await scoreTraces({ mastra, scorerId: 'helpfulness', targets: [{ traceId, spanId }], });
After
import { scoreTrace, scoreTraceBatch } from '@mastra/core/evals/scoreTraces'; const savedScore = await scoreTrace({ storage, scorer, target: { trace: preloadedTrace, spanId }, batchId, datasetId, datasetItemId, }); const result = await scoreTraceBatch({ storage, scorer, targets, batchId, datasetId, });
-
Add optional
batchId,datasetId, anddatasetItemIdfields to persisted scores so saved baseline scores can be grouped as one scoring pass and joined back to the dataset items they came from. (#18331)scoreTrace()accepts top-levelbatchId,datasetId, anddatasetItemIdwhen persisting a score for a stored trace.ScoreRowDataand score save payloads now include nullablebatchId,datasetId, anddatasetItemId.- Built-in stores with explicit score schema or attribute mappings now persist these provenance fields on saved scores.
- D1, DSQL, MSSQL, and Upstash score stores now apply additive provenance migrations or deterministic score ordering for persisted score reads.
await scoreTrace({ storage, scorer, target: { traceId }, batchId: 'baseline-batch-1', datasetId, datasetItemId, });
-
Added multi-tenant scoping to stored scorer definitions. Stored scorers now persist optional
organizationIdandprojectIdon the definition record, andlist/listResolvedaccept matching filters to scope results by tenant. The Postgres adapter backfills the new columns and applies the scoped filters; tenancy lives on the record while version snapshots stay pure config. (#18331)await storage.create({ scorerDefinition: { id, organizationId: 'org-a', projectId: 'proj-1', ...config }, }); const { scorerDefinitions } = await storage.list({ status: 'draft', organizationId: 'org-a', projectId: 'proj-1', });
-
Added optional
organizationIdandprojectIdfields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and thelistScoresBy*methods accept afiltersoption to scope results by organization and project. (#18331)await storage.saveScore({ ...score, organizationId: 'org-a', projectId: 'proj-1' }); const result = await storage.listScoresByScorerId({ scorerId, filters: { organizationId: 'org-a', projectId: 'proj-1' }, });
projectIdidentifies the project scope, separate fromresourceIdwhich continues to mean the agent memory resource. -
Agents using models that dropped support for
temperature,topP, ortopK(such asclaude-opus-4-7orgpt-5-pro) no longer crash with a 400 error. The model router now automatically strips unsupported sampling parameters before the request is sent — no configuration or processors needed. (#18622)const agent = new Agent({ model: 'anthropic/claude-opus-4-7', instructions: 'You are a helpful assistant.', }); // temperature is stripped automatically — no 400 error await agent.generate('hello', { modelSettings: { temperature: 0.7 } });
Patch Changes
-
Fixed signal drain parity for durable agents. Signals sent to a running durable agent (via sendSignal or sendMessage) are now consumed and echoed to the client stream, matching the behavior of regular agents. This includes initial signal echoes on the first model request, pre-run signal drain before the first LLM call, within-iteration signal drain between tool execution and task completion, and inter-iteration signal drain in the loop predicate that forces continuation so the LLM sees newly arrived signals. (#18732)
-
Update provider registry and model documentation with latest models and providers (
0f69865) -
Surface persistence failures in experiment runs. Previously, when
addExperimentResultthrew duringrunExperiment, the failure was silently logged withconsole.warnand the run continued. The item was still counted as succeeded or failed based on the agent run outcome, soExperimentSummary.succeededCountcould report more rows than actually existed inmastra_experiment_results— silent data loss with no signal to the caller. (#18716)Now each item result carries an optional
persistenceError: { message } | nullfield, and the summary exposes an optionalpersistenceFailures: numbercounter. The raw error (including stack) is logged internally via the Mastra logger and intentionally omitted from the returned object so the summary can safely cross trust boundaries (e.g. UIs, API responses) without leaking internal paths. Target-run counters (succeededCount/failedCount) still reflect what the target did, and callers can inspectpersistenceFailuresto detect when the DB is out of sync with the returned summary and decide whether to retry or alert. The persistence failure is also logged via the Mastra logger at error level instead ofconsole.warn.Both fields are optional on the types so external mocks / wrappers don't need to hand-construct them; the runner always populates them (
null/0on the happy path).const summary = await runExperiment(mastra, { datasetId, targetType: 'agent', targetId: 'my-agent' }); if ((summary.persistenceFailures ?? 0) > 0) { const dropped = summary.results.filter(r => r.persistenceError != null); for (const item of dropped) { console.error(`item ${item.itemId} did not persist:`, item.persistenceError?.message); } }
-
Fixed durable agent parity gaps: emit
startchunk for correct stream ordering, handle TripWire from input processors during preparation, and portonInputAvailable/onOutputtool lifecycle hooks to the durable tool execution path. Removed stale test harness guards that were preventingisTaskComplete,actor,savePerStep, andproviderOptionsfrom reaching durable agent runs. These fixes enable 20+ scenario tests to run on the durable engine. (#18806) -
Fixed replay ordering on pull-mode transports (e.g. Redis Streams): history events are now delivered before live events, offsets are enforced on the live path, suppressed duplicates are acknowledged so persistent transports stop redelivering them, and ack/nack handles are preserved for buffered events. (#18479)
-
Added optional filter arguments to
Dataset.listExperiments()andDataset.listExperimentResults(). The storage layer already accepted these filters — they are now reachable from theDatasethandle. All new parameters are optional and existing callers are unaffected. (#18769)Before:
const { experiments } = await dataset.listExperiments({ page: 0, perPage: 10 }); const baselineOnly = experiments.filter(e => e.agentVersion === 'v1');
After:
const { experiments } = await dataset.listExperiments({ targetType: 'agent', targetId: 'my-agent', agentVersion: 'v1', status: 'completed', page: 0, perPage: 10, });
listExperimentsaccepts:targetType,targetId,agentVersion,status, tenancyfilters.
listExperimentResultsaccepts:traceId,status, tenancyfilters.Enables baseline vs variant read patterns without client-side filtering or bypassing
Dataset. -
Added optional tenancy arguments to
getDataset,updateDataset, anddeleteDataset. (#18750)You can now pass
organizationIdandprojectIdto scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throwDATASET_NOT_FOUND(surfaced as a 404 over HTTP). Deletes silently no-op on a tenancy mismatch — matching the existing "delete non-existent id is a no-op" semantics so cross-tenant existence is never leaked via error timing or status.Example
// Before await client.getDataset('abc123'); await client.deleteDataset('abc123'); await client.updateDataset({ id: 'abc123', name: 'renamed' }); // After — scope to a tenant await client.getDataset('abc123', { organizationId: 'org_a', projectId: 'proj_1' }); await client.deleteDataset('abc123', { organizationId: 'org_a' }); await client.updateDataset({ id: 'abc123', name: 'renamed', organizationId: 'org_a' });
-
Fixed RequestContext leaking auth tokens onto scorer_run span input. Added serializeForSpan() to RequestContext so deepClean uses a safe snapshot instead of walking the internal registry Map. Also fixed the MastraScorer.run() call site to pass the serialized snapshot into the span input. (#18776)
-
Pushed remaining dataset read filters and pagination down to storage. (#18710)
DatasetsManager.list({ filters })now acceptstargetType,targetIds(overlap/union semantics), andname(substring, case-insensitive) in addition to the existing tenancy and candidate filters. Filtering is pushed down to the storage layer so callers no longer have to post-filter results.Storage adapters must also be upgraded to the versions listed below to honor the new filters. If a caller is on this version of
@mastra/corebut on an older storage adapter, the newtargetType/targetIds/namefilter keys are silently ignored by the adapter — no runtime error, but the filter has no effect and every dataset in the tenancy is returned.Dataset.listItems({ version, search, page, perPage })now appliessearchand pagination at the storage layer whenversionis provided alongside any of those. Previously they were silently dropped wheneverversionwas set. The return shape is unchanged: passing onlyversionstill returns a bareDatasetItem[]snapshot; passingsearch,page, orperPage(with or withoutversion) returns the paginated{ items, pagination }shape. The bare-array branch is marked@deprecated; prefer passingpage/perPageto always receive the paginated shape. -
Fixed SystemPromptScrubber
processOutputStreamswallowing TripWire errors when strategy isblock. The abort call now correctly propagates the TripWire to halt the agent stream, matching the existing behavior inprocessOutputResult. (#18794) -
Fixed five DurableAgent behavioral parity gaps with the regular Agent loop: (#18712)
- Goal step: Durable agents now honor goal-aware stop semantics. The
goalStephas been ported into the durable workflow, reading goal config, running completion scorers, and emitting goal chunks — matching the regular agent's behavior. - Output processors for tool chunks: Tool-result and tool-error chunks on durable agents now pass through output processors before emission, enabling content moderation and redaction workflows.
- Cached response replay: Input processors that return a cached response via
processLLMRequestnow work on durable agents, short-circuiting the model call and replaying cached chunks. - toModelOutput normalization: Durable agents now call
toModelOutputon successful tool results under a MAPPING observability span and normalize the output to AI SDK format, matching the regular agent's behavior. - Client-tool observability:
onInputStartandonInputDeltacallbacks on tool definitions are now invoked during durable agent streaming, and client-tool observability spans are created for tool input streaming.
- Goal step: Durable agents now honor goal-aware stop semantics. The
-
DurableAgentnow matchesAgentfor several per-step behaviors that were silently degraded on the durable path: (#18693)- Tools suspended for human-in-the-loop now receive the same auto-resume system-message rewrite when
autoResumeSuspendedToolsis enabled. - Agents wired to a
BackgroundTaskManagerget the background-task guidance prompt injected before each LLM call. - Model
supportedUrls(including async resolvers) is honored consistently for both regular and durable runs. - HTTP headers attached to LLM calls (memory routing, model-config, call-time
modelSettings.headers) merge in a single documented order and are case-normalized so call-time values reliably override. prepareStepand input-processor overrides — includingmodel,tools,activeTools,providerOptions,modelSettings,structuredOutput, andworkspace— apply identically on both paths.
- Tools suspended for human-in-the-loop now receive the same auto-resume system-message rewrite when
-
Durable agents now honor
onIterationCompletecallback return values and delegation bail signals in the loop predicate, closing three behavioral parity gaps with the regular agent: (#18707)- Delegation bail — When an
onDelegationCompletehook callsctx.bail(), the durable loop now stops at the next predicate evaluation instead of continuing indefinitely. ThedelegationBailedflag propagates throughDurableAgenticExecutionOutputandbaseIterationStateSchema. onIterationCompletecallback dispatch — The durable predicate now callsonIterationCompletedirectly (read fromglobalRunRegistry) and honors its return value:{ continue: false }stops the loop,{ continue: true }forces continuation whenmaxStepsallows, and{ feedback }injects a user message for the next LLM turn.- Two-phase stop (
pendingFeedbackStop) —onIterationCompletereturning{ continue: false, feedback: '...' }now schedules exactly one more LLM turn before stopping, matching the regular agent's behavior. ThependingFeedbackStopflag is persisted inbaseIterationStateSchemaacross iterations.
Signal drain (bugs 5 and 11) is deferred —
DurableAgentdoes not yet participate inagentThreadStreamRuntimeand has nosendMessage/ signal infrastructure.Scenario tests
delegation-complete-bailandstop-condition-long-loopnow run on the durable engine. Theaimock-scenarioharness no longer dropsstopWhen,delegation, oronIterationCompletefor durable runs. - Delegation bail — When an
-
Background-dispatched sub-agent delegations no longer send null tool-message content (#17791)
When a sub-agent invocation (an
agent-<name>tool) is dispatched as a background task, the agentic loop hands the sub-agent tool'stoModelOutputthe placeholder string fromtool-call-step.ts("Background task started...") instead of theagentOutputSchemaobject.toModelOutputreadoutput.text, which is undefined for that string, so the supervisor's next request carried arole: "tool"message with null content. Providers that validate tool content (e.g. Anthropic) reject that with a 500, breaking the supervisor turn whenever it backgrounds a sub-agent (backgroundTasks.tools: { someSubAgent: { enabled: true } }).toModelOutputnow uses the placeholder string directly when the output is a string, so the tool message always carries non-empty content and the supervisor can acknowledge the dispatch and continue while the sub-agent runs in the background. -
Fix evented workflow parallel steps re-running the wrong branch on restart. The parallel processor built each branch's execution path from its position in the filtered (active-only) list instead of its real index, so restarting a parallel step whose active branches were not a contiguous prefix routed to the wrong branch (and skipped the intended one). Branches are now addressed by their real index. Closes #18754. (#18755)
-
createCodingAgentnow only includes the defaultTaskSignalProviderwhenmemoryis configured. Previously it always wiredTaskSignalProvider, whoseTaskStateProcessorrequires a memory-backed thread — causing a hard error in memoryless contexts. The provider is merged into caller-provided signals when memory is present, so custom signal providers don't drop task tracking. (#18728) -
Fixed approved and declined tool approvals not round-tripping on recall. (#18583)
After a
requireApprovaltool call was approved or declined,memory.recall()lost the decision: a decline was stored as a normal successful result (state: 'result'with the rejection string) and an approval dropped the approval entirely. Now:- Declined calls persist as
state: 'output-denied'withapproval: { id, approved: false, reason }, so recalled AI SDK v6 UI parts render asoutput-denied. In v4 and v5 (which have no denied state) the call downgrades to a singleoutput-available(v5) / result (v4) part whose output is the decline reason — so the agent's onFinish memory save no longer throwsToolInvocation must have a result. - Approved calls keep
approval: { id, approved: true }alongside the result, so v6 UI parts carry the approval.
Approved and declined decisions now round-trip on recall consistently for both standard agents and durable agents (
DurableAgent, including the Inngest durable agent).Live approve/decline already worked; this was a write-path persistence gap. Fixes #17218.
- Declined calls persist as
-
Fixed reasoning text being lost in AIV4Adapter and stream-chunk assembly at the end of the turn (#18534)
-
Tenancy-scope experiments
getByIdanddelete*onExperimentsStorage. (#18770)ExperimentsStorage.getExperimentById,getExperimentResultById,deleteExperiment, anddeleteExperimentResultsused to key on the primary id alone, so any caller who knew the id could read or delete the row regardless of tenant. All four now accept an optionalfilters: { organizationId?, projectId? }argument that is enforced on every adapter (inmemory, libsql, pg, mysql, mongodb, spanner):- On tenancy mismatch,
get*returnsnullat the storage layer. - On tenancy mismatch,
delete*is a silent no-op. - The tenancy predicate is folded into the destructive DML itself (scoped
WHEREon the DELETE, an atomic gate + delete inside a transaction, or a scoped subquery for the results cascade). A concurrent tenant swap of the same id between a pre-check and the DELETE cannot let a scoped delete hit another tenant's row.
Both behaviors match how a missing id already responds, so existence does not leak through error timing or messages.
The same atomic-DML pattern is also applied to
DatasetsStorage.deleteDatasetacross all 5 store adapters, closing a TOCTOU window between the pre-check and the parent DELETE that was introduced when tenancy filters were originally added.Dataset.getExperimentand the shared experiment-ownership gate onDatasetnow forward the dataset's tenancy scope to storage, so experiment reads and downstream mutations (list results, update result, delete experiment) reached through a dataset handle are automatically scoped to the owning tenant.Legacy calls that omit
filtersare unchanged, so this is fully backwards-compatible.// Before: any caller who knew the id could read/delete across tenants. await store.experiments.getExperimentById({ id: experimentId }); await store.experiments.deleteExperiment({ id: experimentId }); // After: pass the caller's scope; wrong tenant gets null / silent no-op. await store.experiments.getExperimentById({ id: experimentId, filters: { organizationId, projectId }, }); await store.experiments.deleteExperiment({ id: experimentId, filters: { organizationId, projectId }, });
- On tenancy mismatch,
-
Fixed a TypeScript error where auth provider instances (for example
new MastraAuthWorkos()) could not be assigned toserver.authorstudio.auth, failing withProperty '#private' is missing(#18682). (#18796)Auth providers are now typed with a new structural
IMastraAuthProviderinterface (exported from@mastra/core/serverand@mastra/auth), so provider packages no longer need a shared class identity with@mastra/core.CompositeAuthalso accepts anyIMastraAuthProviderimplementation. No code changes are required:import { Mastra } from '@mastra/core'; import { MastraAuthWorkos } from '@mastra/auth-workos'; // Previously failed to compile with TS2322, now works without casts export const mastra = new Mastra({ server: { auth: new MastraAuthWorkos(), }, });
-
Updated the ws dependency to ^8.21.0 to pull in fixes for an uninitialized memory disclosure (GHSA-58qx-3vcg-4xpx) and a memory exhaustion denial-of-service (GHSA-96hv-2xvq-fx4p) in the WebSocket server. (#18789)
-
Fixed an internal Mastra instance leaking a scorer hook onto a shared, process-wide emitter. Agents that use a Workspace (or any unwired agent run) built a throwaway internal Mastra whose scorer hook was never removed, so it kept firing on every scorer run and flooded logs with "scorer not found" errors. The scorer still ran correctly on the real Mastra — the noise came from the orphaned handler, which is now released on teardown. (#18763)
-
Scoped
getDatasetByIdanddeleteDatasetto tenancy filters when the caller passesorganizationId/projectId. (#18750)The adapters now push the tenancy predicate into the SQL/query when the new optional
filtersargument is present. Legacy calls that omit tenancy are unchanged. On mismatch,getDatasetByIdreturnsnullanddeleteDatasetis a silent no-op — the cascade delete (dataset items and versions) is gated by a scoped parent pre-check, so cross-tenant data is never touched. -
Added optional
organizationIdandprojectIdquery parameters to the dataset routes. (#18750)GET /datasets/:datasetId,PATCH /datasets/:datasetId, andDELETE /datasets/:datasetIdnow accept optional tenancy query parameters. When provided, they are forwarded tomastra.datasets.get/.deleteand the operation returns 404 if the dataset does not belong to the requested tenant. Requests that omit the query parameters keep their existing behavior.Example
GET /datasets/abc123?organizationId=org_a&projectId=proj_1 DELETE /datasets/abc123?organizationId=org_a -
Fixed durable agents to no longer persist
modelSettings.headersto durable storage. Headers (which may contain sensitive API keys or auth tokens) are now stripped during serialization and kept in-process on theRunRegistryEntry, then merged back at LLM execution time. (#18751)Also fixed missing model-config-level headers in the durable header merge pipeline.
-
Fixed subagent delegations wasting an LLM call on title generation. When a supervisor agent's Memory has generateTitle enabled and delegates to a subagent with no memory of its own, the subagent inherited the supervisor's Memory instance and its generateTitle setting, firing an extra title-generation call for every ephemeral delegation thread that no one ever sees. Title generation is now treated as a top-level thread concern and is suppressed for these ephemeral subagent threads. To generate titles for a subagent's own threads, give that subagent its own memory configuration. (#18761)
@mastra/apple-container@0.2.0
Minor Changes
-
Add an Apple container CLI workspace sandbox provider. (#18643)
import { AppleContainerSandbox } from '@mastra/apple-container'; const sandbox = new AppleContainerSandbox({ id: 'local-apple-container', image: 'node:22-slim', volumes: { [process.cwd()]: '/workspace' }, });
Patch Changes
@mastra/auth@1.1.2
Patch Changes
-
Fixed a TypeScript error where auth provider instances (for example
new MastraAuthWorkos()) could not be assigned toserver.authorstudio.auth, failing withProperty '#private' is missing(#18682). (#18796)Auth providers are now typed with a new structural
IMastraAuthProviderinterface (exported from@mastra/core/serverand@mastra/auth), so provider packages no longer need a shared class identity with@mastra/core.CompositeAuthalso accepts anyIMastraAuthProviderimplementation. No code changes are required:import { Mastra } from '@mastra/core'; import { MastraAuthWorkos } from '@mastra/auth-workos'; // Previously failed to compile with TS2322, now works without casts export const mastra = new Mastra({ server: { auth: new MastraAuthWorkos(), }, });
@mastra/clickhouse@1.11.1
Patch Changes
-
Added optional
organizationIdandprojectIdfields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and thelistScoresBy*methods accept afiltersoption to scope results by organization and project. (#18331)await storage.saveScore({ ...score, organizationId: 'org-a', projectId: 'proj-1' }); const result = await storage.listScoresByScorerId({ scorerId, filters: { organizationId: 'org-a', projectId: 'proj-1' }, });
projectIdidentifies the project scope, separate fromresourceIdwhich continues to mean the agent memory resource.
@mastra/client-js@1.30.0
Minor Changes
-
Added optional tenancy arguments to
getDataset,updateDataset, anddeleteDataset. (#18750)You can now pass
organizationIdandprojectIdto scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throwDATASET_NOT_FOUND(surfaced as a 404 over HTTP). Deletes silently no-op on a tenancy mismatch — matching the existing "delete non-existent id is a no-op" semantics so cross-tenant existence is never leaked via error timing or status.Example
// Before await client.getDataset('abc123'); await client.deleteDataset('abc123'); await client.updateDataset({ id: 'abc123', name: 'renamed' }); // After — scope to a tenant await client.getDataset('abc123', { organizationId: 'org_a', projectId: 'proj_1' }); await client.deleteDataset('abc123', { organizationId: 'org_a' }); await client.updateDataset({ id: 'abc123', name: 'renamed', organizationId: 'org_a' });
Patch Changes
-
Fixed a cross-tenant data-access issue on datasets by scoping
DatasetsManager.getandDatasetsManager.deleteto tenancy filters. (#18750)Previously
get({ id })anddelete({ id })looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of whichorganizationId/projectIdit belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).What changed
DatasetsManager.getandDatasetsManager.deleteaccept optionalorganizationIdandprojectId.- The tenancy is stashed on the returned
Datasethandle and forwarded to every downstream storage call (getDetails,update,addItem, item batch ops,startExperimentAsync). - The abstract storage contract (
getDatasetById,deleteDataset) gained an optionalfilters?: DatasetTenancyFiltersarg. - Item-mutation inputs (
AddDatasetItemInput,UpdateDatasetItemInput,BatchInsertItemsInput,BatchDeleteItemsInput) andUpdateDatasetInputaccept optionalfiltersfor the internal existence check.
Behavior
- Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
- On tenancy mismatch,
getthrows NOT_FOUND (returns null at the storage layer) anddeleteis a silent no-op — matching how a missing id already behaves, so existence does not leak through error timing or messages.
Example
// Before const ds = await mastra.datasets.get({ id }); await mastra.datasets.delete({ id }); // After — scope to a tenant const ds = await mastra.datasets.get({ id, organizationId, projectId }); await mastra.datasets.delete({ id, organizationId, projectId });
-
Scoped
getDatasetByIdanddeleteDatasetto tenancy filters when the caller passesorganizationId/projectId. (#18750)The adapters now push the tenancy predicate into the SQL/query when the new optional
filtersargument is present. Legacy calls that omit tenancy are unchanged. On mismatch,getDatasetByIdreturnsnullanddeleteDatasetis a silent no-op — the cascade delete (dataset items and versions) is gated by a scoped parent pre-check, so cross-tenant data is never touched. -
Added optional
organizationIdandprojectIdquery parameters to the dataset routes. (#18750)GET /datasets/:datasetId,PATCH /datasets/:datasetId, andDELETE /datasets/:datasetIdnow accept optional tenancy query parameters. When provided, they are forwarded tomastra.datasets.get/.deleteand the operation returns 404 if the dataset does not belong to the requested tenant. Requests that omit the query parameters keep their existing behavior.Example
GET /datasets/abc123?organizationId=org_a&projectId=proj_1 DELETE /datasets/abc123?organizationId=org_a
@mastra/cloudflare@1.5.1
Patch Changes
-
Added optional
organizationIdandprojectIdfields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and thelistScoresBy*methods accept afiltersoption to scope results by organization and project. (#18331)await storage.saveScore({ ...score, organizationId: 'org-a', projectId: 'proj-1' }); const result = await storage.listScoresByScorerId({ scorerId, filters: { organizationId: 'org-a', projectId: 'proj-1' }, });
projectIdidentifies the project scope, separate fromresourceIdwhich continues to mean the agent memory resource.
@mastra/cloudflare-d1@1.1.1
Patch Changes
-
Add optional
batchId,datasetId, anddatasetItemIdfields to persisted scores so saved baseline scores can be grouped as one scoring pass and joined back to the dataset items they came from. (#18331)scoreTrace()accepts top-levelbatchId,datasetId, anddatasetItemIdwhen persisting a score for a stored trace.ScoreRowDataand score save payloads now include nullablebatchId,datasetId, anddatasetItemId.- Built-in stores with explicit score schema or attribute mappings now persist these provenance fields on saved scores.
- D1, DSQL, MSSQL, and Upstash score stores now apply additive provenance migrations or deterministic score ordering for persisted score reads.
await scoreTrace({ storage, scorer, target: { traceId }, batchId: 'baseline-batch-1', datasetId, datasetItemId, });
-
Added optional
organizationIdandprojectIdfields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and thelistScoresBy*methods accept afiltersoption to scope results by organization and project. (#18331)await storage.saveScore({ ...score, organizationId: 'org-a', projectId: 'proj-1' }); const result = await storage.listScoresByScorerId({ scorerId, filters: { organizationId: 'org-a', projectId: 'proj-1' }, });
projectIdidentifies the project scope, separate fromresourceIdwhich continues to mean the agent memory resource.
@mastra/codemod@1.1.1
Patch Changes
- Hardened child-process invocations against shell command injection. Package installs and codemod runs now pass arguments as arrays instead of interpolating them into shell strings, the deployer's shared child-process logger rejects arguments containing shell metacharacters, and the login dialog validates auth URLs and opens the browser without a shell. As a side effect,
mastra codemodnow works on project paths containing spaces. (#18804)
@mastra/convex@1.3.1
Patch Changes
-
Added optional
organizationIdandprojectIdfields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and thelistScoresBy*methods accept afiltersoption to scope results by organization and project. (#18331)await storage.saveScore({ ...score, organizationId: 'org-a', projectId: 'proj-1' }); const result = await storage.listScoresByScorerId({ scorerId, filters: { organizationId: 'org-a', projectId: 'proj-1' }, });
projectIdidentifies the project scope, separate fromresourceIdwhich continues to mean the agent memory resource.
@mastra/deployer@1.49.0
Minor Changes
-
File-based agents can now nest subagents up to three levels deep. A subagent directory can declare its own
subagents/, and each level is assembled and wired into its parent as a delegation tool. Levels deeper than the cap are ignored with a warning. (#18780)src/mastra/agents/ supervisor/ # depth 0 subagents/ researcher/ # depth 1 subagents/ summarizer/ # depth 2
Patch Changes
-
Preserve npm alias dependency specs in generated deployer package.json files. (#18773)
-
Hardened child-process invocations against shell command injection. Package installs and codemod runs now pass arguments as arrays instead of interpolating them into shell strings, the deployer's shared child-process logger rejects arguments containing shell metacharacters, and the login dialog validates auth URLs and opens the browser without a shell. As a side effect,
mastra codemodnow works on project paths containing spaces. (#18804) -
Fix
injectStudioHtmlConfigcorrupting Studio config values that contain$. Values such as request context presets are now injected into the HTML verbatim instead of being mangled byString.prototype.replacespecial patterns ($$,$&, etc.). Closes #18685. (#18686) -
Updated the ws dependency to ^8.21.0 to pull in fixes for an uninitialized memory disclosure (GHSA-58qx-3vcg-4xpx) and a memory exhaustion denial-of-service (GHSA-96hv-2xvq-fx4p) in the WebSocket server. (#18789)
-
Copy pnpm install policy from the source workspace into generated deployer output so pnpm 11 build-script approvals are preserved. (#18772)
-
Signals now show live Entity-Learning data (#18699)
The Signals page is no longer static. Select an agent reported by the platform and Signals fetches that agent's signals and their clusters live from the Entity-Learning API, replacing the previous hardcoded mock data. Each available signal loads its real clusters (topics) and traces, with a scatter-plot chart for the selected topics.
What changed
- Added an agent filter at the top of the Signals page, mirroring the traces filter, so you can inspect signals for any agent on the server.
- The Signals overview and details pages now render live Entity-Learning topics, examples, and points directly, with shape-matching skeletons while data loads, centered empty states, and explicit error states.
- Clicking a cluster card opens its topic by default, and the Signals breadcrumbs preserve the selected entity and topic query params on back-navigation.
- Signals detail navigation keeps selected clusters, trace examples, and chart filters in sync when moving between signals or entities.
Gating
Studio's served HTML exposes
MASTRA_ORGANIZATION_ID,MASTRA_PLATFORM_PROJECT_ID, andMASTRA_PLATFORM_OBSERVABILITY_ENDPOINTto the browser so the Signals page can call the Entity-Learning API. The route is gated on the platform observability config, and theMASTRA_SIGNALS_UIflag guards the sidebar Signals nav link.
@mastra/deployer-vercel@1.2.4
Patch Changes
-
Signals now show live Entity-Learning data (#18699)
The Signals page is no longer static. Select an agent reported by the platform and Signals fetches that agent's signals and their clusters live from the Entity-Learning API, replacing the previous hardcoded mock data. Each available signal loads its real clusters (topics) and traces, with a scatter-plot chart for the selected topics.
What changed
- Added an agent filter at the top of the Signals page, mirroring the traces filter, so you can inspect signals for any agent on the server.
- The Signals overview and details pages now render live Entity-Learning topics, examples, and points directly, with shape-matching skeletons while data loads, centered empty states, and explicit error states.
- Clicking a cluster card opens its topic by default, and the Signals breadcrumbs preserve the selected entity and topic query params on back-navigation.
- Signals detail navigation keeps selected clusters, trace examples, and chart filters in sync when moving between signals or entities.
Gating
Studio's served HTML exposes
MASTRA_ORGANIZATION_ID,MASTRA_PLATFORM_PROJECT_ID, andMASTRA_PLATFORM_OBSERVABILITY_ENDPOINTto the browser so the Signals page can call the Entity-Learning API. The route is gated on the platform observability config, and theMASTRA_SIGNALS_UIflag guards the sidebar Signals nav link.
@mastra/dsql@1.1.2
Patch Changes
-
Add optional
batchId,datasetId, anddatasetItemIdfields to persisted scores so saved baseline scores can be grouped as one scoring pass and joined back to the dataset items they came from. (#18331)scoreTrace()accepts top-levelbatchId,datasetId, anddatasetItemIdwhen persisting a score for a stored trace.ScoreRowDataand score save payloads now include nullablebatchId,datasetId, anddatasetItemId.- Built-in stores with explicit score schema or attribute mappings now persist these provenance fields on saved scores.
- D1, DSQL, MSSQL, and Upstash score stores now apply additive provenance migrations or deterministic score ordering for persisted score reads.
await scoreTrace({ storage, scorer, target: { traceId }, batchId: 'baseline-batch-1', datasetId, datasetItemId, });
-
Added optional
organizationIdandprojectIdfields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and thelistScoresBy*methods accept afiltersoption to scope results by organization and project. (#18331)await storage.saveScore({ ...score, organizationId: 'org-a', projectId: 'proj-1' }); const result = await storage.listScoresByScorerId({ scorerId, filters: { organizationId: 'org-a', projectId: 'proj-1' }, });
projectIdidentifies the project scope, separate fromresourceIdwhich continues to mean the agent memory resource.
@mastra/dynamodb@1.1.2
Patch Changes
-
Add optional
batchId,datasetId, anddatasetItemIdfields to persisted scores so saved baseline scores can be grouped as one scoring pass and joined back to the dataset items they came from. (#18331)scoreTrace()accepts top-levelbatchId,datasetId, anddatasetItemIdwhen persisting a score for a stored trace.ScoreRowDataand score save payloads now include nullablebatchId,datasetId, anddatasetItemId.- Built-in stores with explicit score schema or attribute mappings now persist these provenance fields on saved scores.
- D1, DSQL, MSSQL, and Upstash score stores now apply additive provenance migrations or deterministic score ordering for persisted score reads.
await scoreTrace({ storage, scorer, target: { traceId }, batchId: 'baseline-batch-1', datasetId, datasetItemId, });
-
Added optional
organizationIdandprojectIdfields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and thelistScoresBy*methods accept afiltersoption to scope results by organization and project. (#18331)await storage.saveScore({ ...score, organizationId: 'org-a', projectId: 'proj-1' }); const result = await storage.listScoresByScorerId({ scorerId, filters: { organizationId: 'org-a', projectId: 'proj-1' }, });
projectIdidentifies the project scope, separate fromresourceIdwhich continues to mean the agent memory resource.
@mastra/e2b@0.5.0
Minor Changes
-
Add an E2B sandbox
networkoption that is forwarded toSandbox.create. (#18785)This lets Mastra users configure E2B network controls and per-host request transforms, including
network.rulesheader injection for brokered credentials, throughE2BSandboxwithout wrapping or monkey-patching the E2B SDK.E2B docs: https://e2b.dev/docs/network/internet-access#per-host-request-transforms
Patch Changes
@mastra/hono@1.5.4
Patch Changes
- Updated the ws dependency to ^8.21.0 to pull in fixes for an uninitialized memory disclosure (GHSA-58qx-3vcg-4xpx) and a memory exhaustion denial-of-service (GHSA-96hv-2xvq-fx4p) in the WebSocket server. (#18789)
@mastra/inngest@1.8.1
Patch Changes
- Removed the unused @opentelemetry/core dependency. It was left over from an earlier tracing workaround and pulled a vulnerable version (GHSA-8988-4f7v-96qf) into installs. (#18793)
@mastra/lance@1.1.2
Patch Changes
-
Add optional
batchId,datasetId, anddatasetItemIdfields to persisted scores so saved baseline scores can be grouped as one scoring pass and joined back to the dataset items they came from. (#18331)scoreTrace()accepts top-levelbatchId,datasetId, anddatasetItemIdwhen persisting a score for a stored trace.ScoreRowDataand score save payloads now include nullablebatchId,datasetId, anddatasetItemId.- Built-in stores with explicit score schema or attribute mappings now persist these provenance fields on saved scores.
- D1, DSQL, MSSQL, and Upstash score stores now apply additive provenance migrations or deterministic score ordering for persisted score reads.
await scoreTrace({ storage, scorer, target: { traceId }, batchId: 'baseline-batch-1', datasetId, datasetItemId, });
-
Added optional
organizationIdandprojectIdfields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and thelistScoresBy*methods accept afiltersoption to scope results by organization and project. (#18331)await storage.saveScore({ ...score, organizationId: 'org-a', projectId: 'proj-1' }); const result = await storage.listScoresByScorerId({ scorerId, filters: { organizationId: 'org-a', projectId: 'proj-1' }, });
projectIdidentifies the project scope, separate fromresourceIdwhich continues to mean the agent memory resource.
@mastra/libsql@1.15.0
Minor Changes
-
Added storage retention support to libSQL. When you set a
retentionconfig,LibSQLStorecan prune old rows from every growth-table domain:memory(threads, messages, resources bycreatedAt),threadState(byupdatedAt),observability(spans bystartedAt),scores(bycreatedAt),workflows(run snapshots byupdatedAt),backgroundTasks(bycompletedAt, so in-flight tasks are never pruned),experiments(whole runs bycompletedAt, results cascade with their parent),notificationsandharnesssessions (bycreatedAt), andschedulesfire history (byactual_fire_at). (#18733)Deletes run in batches so they stay safe on large tables. Anchor-column indexes are created lazily on the first
prune()call — never at init — so deployments that don't configure retention pay no extra index overhead.prune()only deletes rows; reclaiming disk (for example aVACUUMon self-hosted libSQL) is left to you to run in a maintenance window.const storage = new LibSQLStore({ id: 'mastra-storage', url: 'file:./mastra.db', retention: { memory: { messages: { maxAge: '30d' } }, observability: { spans: { maxAge: '7d' } }, }, }); await storage.prune();
Patch Changes
-
Added optional tenancy arguments to
getDataset,updateDataset, anddeleteDataset. (#18750)You can now pass
organizationIdandprojectIdto scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throwDATASET_NOT_FOUND(surfaced as a 404 over HTTP). Deletes silently no-op on a tenancy mismatch — matching the existing "delete non-existent id is a no-op" semantics so cross-tenant existence is never leaked via error timing or status.Example
// Before await client.getDataset('abc123'); await client.deleteDataset('abc123'); await client.updateDataset({ id: 'abc123', name: 'renamed' }); // After — scope to a tenant await client.getDataset('abc123', { organizationId: 'org_a', projectId: 'proj_1' }); await client.deleteDataset('abc123', { organizationId: 'org_a' }); await client.updateDataset({ id: 'abc123', name: 'renamed', organizationId: 'org_a' });
-
Pushed remaining dataset read filters and pagination down to storage. (#18710)
DatasetsManager.list({ filters })now acceptstargetType,targetIds(overlap/union semantics), andname(substring, case-insensitive) in addition to the existing tenancy and candidate filters. Filtering is pushed down to the storage layer so callers no longer have to post-filter results.Storage adapters must also be upgraded to the versions listed below to honor the new filters. If a caller is on this version of
@mastra/corebut on an older storage adapter, the newtargetType/targetIds/namefilter keys are silently ignored by the adapter — no runtime error, but the filter has no effect and every dataset in the tenancy is returned.Dataset.listItems({ version, search, page, perPage })now appliessearchand pagination at the storage layer whenversionis provided alongside any of those. Previously they were silently dropped wheneverversionwas set. The return shape is unchanged: passing onlyversionstill returns a bareDatasetItem[]snapshot; passingsearch,page, orperPage(with or withoutversion) returns the paginated{ items, pagination }shape. The bare-array branch is marked@deprecated; prefer passingpage/perPageto always receive the paginated shape. -
Fixed a double-encoding bug where
createDatasetstoredtargetIdsandscorerIdsas JSON-encoded strings instead of arrays. This caused the newlistDatasets({ filters: { targetIds } })overlap query to never match. (#18710)Existing rows written before this fix are still double-encoded and will not be matched by the new
targetIdsfilter. They self-heal on the nextupdateDatasetcall. Deployments with a long tail of pre-existing datasets should run a one-time backfill (re-encodingtargetIdsandscorerIdson affected rows) rather than rely on incidental writes; this can be tracked as a follow-up if needed. -
Tenancy-scope experiments
getByIdanddelete*onExperimentsStorage. (#18770)ExperimentsStorage.getExperimentById,getExperimentResultById,deleteExperiment, anddeleteExperimentResultsused to key on the primary id alone, so any caller who knew the id could read or delete the row regardless of tenant. All four now accept an optionalfilters: { organizationId?, projectId? }argument that is enforced on every adapter (inmemory, libsql, pg, mysql, mongodb, spanner):- On tenancy mismatch,
get*returnsnullat the storage layer. - On tenancy mismatch,
delete*is a silent no-op. - The tenancy predicate is folded into the destructive DML itself (scoped
WHEREon the DELETE, an atomic gate + delete inside a transaction, or a scoped subquery for the results cascade). A concurrent tenant swap of the same id between a pre-check and the DELETE cannot let a scoped delete hit another tenant's row.
Both behaviors match how a missing id already responds, so existence does not leak through error timing or messages.
The same atomic-DML pattern is also applied to
DatasetsStorage.deleteDatasetacross all 5 store adapters, closing a TOCTOU window between the pre-check and the parent DELETE that was introduced when tenancy filters were originally added.Dataset.getExperimentand the shared experiment-ownership gate onDatasetnow forward the dataset's tenancy scope to storage, so experiment reads and downstream mutations (list results, update result, delete experiment) reached through a dataset handle are automatically scoped to the owning tenant.Legacy calls that omit
filtersare unchanged, so this is fully backwards-compatible.// Before: any caller who knew the id could read/delete across tenants. await store.experiments.getExperimentById({ id: experimentId }); await store.experiments.deleteExperiment({ id: experimentId }); // After: pass the caller's scope; wrong tenant gets null / silent no-op. await store.experiments.getExperimentById({ id: experimentId, filters: { organizationId, projectId }, }); await store.experiments.deleteExperiment({ id: experimentId, filters: { organizationId, projectId }, });
- On tenancy mismatch,
-
Fixed a cross-tenant data-access issue on datasets by scoping
DatasetsManager.getandDatasetsManager.deleteto tenancy filters. (#18750)Previously
get({ id })anddelete({ id })looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of whichorganizationId/projectIdit belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).What changed
DatasetsManager.getandDatasetsManager.deleteaccept optionalorganizationIdandprojectId.- The tenancy is stashed on the returned
Datasethandle and forwarded to every downstream storage call (getDetails,update,addItem, item batch ops,startExperimentAsync). - The abstract storage contract (
getDatasetById,deleteDataset) gained an optionalfilters?: DatasetTenancyFiltersarg. - Item-mutation inputs (
AddDatasetItemInput,UpdateDatasetItemInput,BatchInsertItemsInput,BatchDeleteItemsInput) andUpdateDatasetInputaccept optionalfiltersfor the internal existence check.
Behavior
- Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
- On tenancy mismatch,
getthrows NOT_FOUND (returns null at the storage layer) anddeleteis a silent no-op — matching how a missing id already behaves, so existence does not leak through error timing or messages.
Example
// Before const ds = await mastra.datasets.get({ id }); await mastra.datasets.delete({ id }); // After — scope to a tenant const ds = await mastra.datasets.get({ id, organizationId, projectId }); await mastra.datasets.delete({ id, organizationId, projectId });
-
Add optional
batchId,datasetId, anddatasetItemIdfields to persisted scores so saved baseline scores can be grouped as one scoring pass and joined back to the dataset items they came from. (#18331)scoreTrace()accepts top-levelbatchId,datasetId, anddatasetItemIdwhen persisting a score for a stored trace.ScoreRowDataand score save payloads now include nullablebatchId,datasetId, anddatasetItemId.- Built-in stores with explicit score schema or attribute mappings now persist these provenance fields on saved scores.
- D1, DSQL, MSSQL, and Upstash score stores now apply additive provenance migrations or deterministic score ordering for persisted score reads.
await scoreTrace({ storage, scorer, target: { traceId }, batchId: 'baseline-batch-1', datasetId, datasetItemId, });
-
Added optional
organizationIdandprojectIdfields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and thelistScoresBy*methods accept afiltersoption to scope results by organization and project. (#18331)await storage.saveScore({ ...score, organizationId: 'org-a', projectId: 'proj-1' }); const result = await storage.listScoresByScorerId({ scorerId, filters: { organizationId: 'org-a', projectId: 'proj-1' }, });
projectIdidentifies the project scope, separate fromresourceIdwhich continues to mean the agent memory resource. -
Raise
@mastra/corepeer floor to>=1.49.0-0on all storage adapters so the tenancy-related named exports the adapters now consume are guaranteed to exist at install time. (#18861) -
Scoped
getDatasetByIdanddeleteDatasetto tenancy filters when the caller passesorganizationId/projectId. (#18750)The adapters now push the tenancy predicate into the SQL/query when the new optional
filtersargument is present. Legacy calls that omit tenancy are unchanged. On mismatch,getDatasetByIdreturnsnullanddeleteDatasetis a silent no-op — the cascade delete (dataset items and versions) is gated by a scoped parent pre-check, so cross-tenant data is never touched. -
Added optional
organizationIdandprojectIdquery parameters to the dataset routes. (#18750)GET /datasets/:datasetId,PATCH /datasets/:datasetId, andDELETE /datasets/:datasetIdnow accept optional tenancy query parameters. When provided, they are forwarded tomastra.datasets.get/.deleteand the operation returns 404 if the dataset does not belong to the requested tenant. Requests that omit the query parameters keep their existing behavior.Example
GET /datasets/abc123?organizationId=org_a&projectId=proj_1 DELETE /datasets/abc123?organizationId=org_a
@mastra/mcp@1.13.0
Minor Changes
-
Fixed MCP tool execution failures being recorded as successes. (#18482)
A failing MCP tool used to look like it succeeded. The call was traced and saved as a success, and error handling like retries and Studio error states never ran. For tools with an
outputSchema, the error message was thrown away, so neither the model nor the user saw why the call failed.This happened because the server reports the failure inside a normal result (with an
isErrorflag), andMCPClientdid not check that flag.Now
isError: trueresults are surfaced on the failed-tool-call path: the tool throws with the server's error text, so spans, stream chunks, scorers, and persisted messages reflect the failure and the model can self-correct.You can opt back into the previous behavior per server with
onToolError: 'return', which resolves with the raw result instead of throwing:const mcp = new MCPClient({ servers: { weather: { url: new URL('https://example.com/mcp'), onToolError: 'return', // default is 'throw' }, }, });
Patch Changes
- Security hardening from CodeQL review: MCP serverless 500 responses no longer echo internal error messages to clients (details are still logged server-side), and macOS system notifications now escape backslashes and run osascript without a shell so notification text can't inject commands. (#18805)
@mastra/memory@1.22.1
Patch Changes
- Fixed observational memory token counting when recalled messages include a declined tool approval. Memory processing no longer errors on those messages. (#18583)
@mastra/mesa@0.2.0
Minor Changes
-
Added a Mesa filesystem provider for Mastra workspaces. (#18740)
import { Workspace } from '@mastra/core/workspace'; import { MesaFilesystem } from '@mastra/mesa'; const workspace = new Workspace({ filesystem: new MesaFilesystem({ apiKey: process.env.MESA_API_KEY, org: 'acme', repos: [{ name: 'docs', bookmark: 'main' }], }), });
Patch Changes
@mastra/mongodb@1.12.0
Minor Changes
-
Added storage retention support to MongoDB. When you set a
retentionconfig,MongoDBStorecan prune old documents from every growth domain it implements:memory(threads, messages, resources bycreatedAt),observability(spans bystartedAt),scores(bycreatedAt),workflows(run snapshots byupdatedAt),backgroundTasks(bycompletedAt, so in-flight tasks are never pruned),experiments(whole runs bycompletedAt, results cascade with their parent — transactional on replica sets),notifications(bycreatedAt), andschedulesfire history (byactual_fire_at). (#18798)Deletes run in batches via bounded
find(_id)+deleteManypairs (bounded, resumable, and cancellable) so they stay safe on large collections. Anchor-field indexes are created lazily on the firstprune()call — never at init — so deployments that don't configure retention pay no extra index overhead.prune()only deletes documents; WiredTiger reuses the freed space for subsequent writes.const storage = new MongoDBStore({ id: 'mastra-storage', uri: process.env.MONGODB_URI, dbName: 'mastra', retention: { memory: { messages: { maxAge: '30d' } }, observability: { spans: { maxAge: '7d' } }, }, }); await storage.prune();
Patch Changes
-
Added optional tenancy arguments to
getDataset,updateDataset, anddeleteDataset. (#18750)You can now pass
organizationIdandprojectIdto scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throwDATASET_NOT_FOUND(surfaced as a 404 over HTTP). Deletes silently no-op on a tenancy mismatch — matching the existing "delete non-existent id is a no-op" semantics so cross-tenant existence is never leaked via error timing or status.Example
// Before await client.getDataset('abc123'); await client.deleteDataset('abc123'); await client.updateDataset({ id: 'abc123', name: 'renamed' }); // After — scope to a tenant await client.getDataset('abc123', { organizationId: 'org_a', projectId: 'proj_1' }); await client.deleteDataset('abc123', { organizationId: 'org_a' }); await client.updateDataset({ id: 'abc123', name: 'renamed', organizationId: 'org_a' });
-
Pushed remaining dataset read filters and pagination down to storage. (#18710)
DatasetsManager.list({ filters })now acceptstargetType,targetIds(overlap/union semantics), andname(substring, case-insensitive) in addition to the existing tenancy and candidate filters. Filtering is pushed down to the storage layer so callers no longer have to post-filter results.Storage adapters must also be upgraded to the versions listed below to honor the new filters. If a caller is on this version of
@mastra/corebut on an older storage adapter, the newtargetType/targetIds/namefilter keys are silently ignored by the adapter — no runtime error, but the filter has no effect and every dataset in the tenancy is returned.Dataset.listItems({ version, search, page, perPage })now appliessearchand pagination at the storage layer whenversionis provided alongside any of those. Previously they were silently dropped wheneverversionwas set. The return shape is unchanged: passing onlyversionstill returns a bareDatasetItem[]snapshot; passingsearch,page, orperPage(with or withoutversion) returns the paginated{ items, pagination }shape. The bare-array branch is marked@deprecated; prefer passingpage/perPageto always receive the paginated shape. -
Tenancy-scope experiments
getByIdanddelete*onExperimentsStorage. (#18770)ExperimentsStorage.getExperimentById,getExperimentResultById,deleteExperiment, anddeleteExperimentResultsused to key on the primary id alone, so any caller who knew the id could read or delete the row regardless of tenant. All four now accept an optionalfilters: { organizationId?, projectId? }argument that is enforced on every adapter (inmemory, libsql, pg, mysql, mongodb, spanner):- On tenancy mismatch,
get*returnsnullat the storage layer. - On tenancy mismatch,
delete*is a silent no-op. - The tenancy predicate is folded into the destructive DML itself (scoped
WHEREon the DELETE, an atomic gate + delete inside a transaction, or a scoped subquery for the results cascade). A concurrent tenant swap of the same id between a pre-check and the DELETE cannot let a scoped delete hit another tenant's row.
Both behaviors match how a missing id already responds, so existence does not leak through error timing or messages.
The same atomic-DML pattern is also applied to
DatasetsStorage.deleteDatasetacross all 5 store adapters, closing a TOCTOU window between the pre-check and the parent DELETE that was introduced when tenancy filters were originally added.Dataset.getExperimentand the shared experiment-ownership gate onDatasetnow forward the dataset's tenancy scope to storage, so experiment reads and downstream mutations (list results, update result, delete experiment) reached through a dataset handle are automatically scoped to the owning tenant.Legacy calls that omit
filtersare unchanged, so this is fully backwards-compatible.// Before: any caller who knew the id could read/delete across tenants. await store.experiments.getExperimentById({ id: experimentId }); await store.experiments.deleteExperiment({ id: experimentId }); // After: pass the caller's scope; wrong tenant gets null / silent no-op. await store.experiments.getExperimentById({ id: experimentId, filters: { organizationId, projectId }, }); await store.experiments.deleteExperiment({ id: experimentId, filters: { organizationId, projectId }, });
- On tenancy mismatch,
-
Fixed
createExperimentin the MongoDB store persistingagentVersionasnullregardless of the input.listExperimentsalready accepts anagentVersionfilter, but rows created by this backend would never match it. New experiments now round-tripagentVersionend-to-end. (#18769) -
Fixed a cross-tenant data-access issue on datasets by scoping
DatasetsManager.getandDatasetsManager.deleteto tenancy filters. (#18750)Previously
get({ id })anddelete({ id })looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of whichorganizationId/projectIdit belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).What changed
DatasetsManager.getandDatasetsManager.deleteaccept optionalorganizationIdandprojectId.- The tenancy is stashed on the returned
Datasethandle and forwarded to every downstream storage call (getDetails,update,addItem, item batch ops,startExperimentAsync). - The abstract storage contract (
getDatasetById,deleteDataset) gained an optionalfilters?: DatasetTenancyFiltersarg. - Item-mutation inputs (
AddDatasetItemInput,UpdateDatasetItemInput,BatchInsertItemsInput,BatchDeleteItemsInput) andUpdateDatasetInputaccept optionalfiltersfor the internal existence check.
Behavior
- Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
- On tenancy mismatch,
getthrows NOT_FOUND (returns null at the storage layer) anddeleteis a silent no-op — matching how a missing id already behaves, so existence does not leak through error timing or messages.
Example
// Before const ds = await mastra.datasets.get({ id }); await mastra.datasets.delete({ id }); // After — scope to a tenant const ds = await mastra.datasets.get({ id, organizationId, projectId }); await mastra.datasets.delete({ id, organizationId, projectId });
-
Added optional
organizationIdandprojectIdfields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and thelistScoresBy*methods accept afiltersoption to scope results by organization and project. (#18331)await storage.saveScore({ ...score, organizationId: 'org-a', projectId: 'proj-1' }); const result = await storage.listScoresByScorerId({ scorerId, filters: { organizationId: 'org-a', projectId: 'proj-1' }, });
projectIdidentifies the project scope, separate fromresourceIdwhich continues to mean the agent memory resource. -
Raise
@mastra/corepeer floor to>=1.49.0-0on all storage adapters so the tenancy-related named exports the adapters now consume are guaranteed to exist at install time. (#18861) -
Scoped
getDatasetByIdanddeleteDatasetto tenancy filters when the caller passesorganizationId/projectId. (#18750)The adapters now push the tenancy predicate into the SQL/query when the new optional
filtersargument is present. Legacy calls that omit tenancy are unchanged. On mismatch,getDatasetByIdreturnsnullanddeleteDatasetis a silent no-op — the cascade delete (dataset items and versions) is gated by a scoped parent pre-check, so cross-tenant data is never touched. -
Added optional
organizationIdandprojectIdquery parameters to the dataset routes. (#18750)GET /datasets/:datasetId,PATCH /datasets/:datasetId, andDELETE /datasets/:datasetIdnow accept optional tenancy query parameters. When provided, they are forwarded tomastra.datasets.get/.deleteand the operation returns 404 if the dataset does not belong to the requested tenant. Requests that omit the query parameters keep their existing behavior.Example
GET /datasets/abc123?organizationId=org_a&projectId=proj_1 DELETE /datasets/abc123?organizationId=org_a
@mastra/mssql@1.4.1
Patch Changes
-
Add optional
batchId,datasetId, anddatasetItemIdfields to persisted scores so saved baseline scores can be grouped as one scoring pass and joined back to the dataset items they came from. (#18331)scoreTrace()accepts top-levelbatchId,datasetId, anddatasetItemIdwhen persisting a score for a stored trace.ScoreRowDataand score save payloads now include nullablebatchId,datasetId, anddatasetItemId.- Built-in stores with explicit score schema or attribute mappings now persist these provenance fields on saved scores.
- D1, DSQL, MSSQL, and Upstash score stores now apply additive provenance migrations or deterministic score ordering for persisted score reads.
await scoreTrace({ storage, scorer, target: { traceId }, batchId: 'baseline-batch-1', datasetId, datasetItemId, });
-
Added optional
organizationIdandprojectIdfields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and thelistScoresBy*methods accept afiltersoption to scope results by organization and project. (#18331)await storage.saveScore({ ...score, organizationId: 'org-a', projectId: 'proj-1' }); const result = await storage.listScoresByScorerId({ scorerId, filters: { organizationId: 'org-a', projectId: 'proj-1' }, });
projectIdidentifies the project scope, separate fromresourceIdwhich continues to mean the agent memory resource.
@mastra/mysql@0.3.3
Patch Changes
-
Added optional tenancy arguments to
getDataset,updateDataset, anddeleteDataset. (#18750)You can now pass
organizationIdandprojectIdto scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throwDATASET_NOT_FOUND(surfaced as a 404 over HTTP). Deletes silently no-op on a tenancy mismatch — matching the existing "delete non-existent id is a no-op" semantics so cross-tenant existence is never leaked via error timing or status.Example
// Before await client.getDataset('abc123'); await client.deleteDataset('abc123'); await client.updateDataset({ id: 'abc123', name: 'renamed' }); // After — scope to a tenant await client.getDataset('abc123', { organizationId: 'org_a', projectId: 'proj_1' }); await client.deleteDataset('abc123', { organizationId: 'org_a' }); await client.updateDataset({ id: 'abc123', name: 'renamed', organizationId: 'org_a' });
-
Fixed
listExperimentsin the MySQL store ignoringtargetType,targetId,agentVersion, andstatusfilters. Queries now correctly narrow on these fields, matching the behavior of the other stores (Postgres, LibSQL, Spanner, in-memory). (#18769)Also persisted
agentVersionon experiment rows in the MySQL store. The column existed in the schema butcreateExperimentnever wrote it andgetExperimentById/listExperimentsnever returned it, so filtering byagentVersionwould have matched nothing on rows created by this backend. New experiments now round-tripagentVersionend-to-end. Existing tables gain the column via theinit()backfill. -
Pushed remaining dataset read filters and pagination down to storage. (#18710)
DatasetsManager.list({ filters })now acceptstargetType,targetIds(overlap/union semantics), andname(substring, case-insensitive) in addition to the existing tenancy and candidate filters. Filtering is pushed down to the storage layer so callers no longer have to post-filter results.Storage adapters must also be upgraded to the versions listed below to honor the new filters. If a caller is on this version of
@mastra/corebut on an older storage adapter, the newtargetType/targetIds/namefilter keys are silently ignored by the adapter — no runtime error, but the filter has no effect and every dataset in the tenancy is returned.Dataset.listItems({ version, search, page, perPage })now appliessearchand pagination at the storage layer whenversionis provided alongside any of those. Previously they were silently dropped wheneverversionwas set. The return shape is unchanged: passing onlyversionstill returns a bareDatasetItem[]snapshot; passingsearch,page, orperPage(with or withoutversion) returns the paginated{ items, pagination }shape. The bare-array branch is marked@deprecated; prefer passingpage/perPageto always receive the paginated shape. -
Tenancy-scope experiments
getByIdanddelete*onExperimentsStorage. (#18770)ExperimentsStorage.getExperimentById,getExperimentResultById,deleteExperiment, anddeleteExperimentResultsused to key on the primary id alone, so any caller who knew the id could read or delete the row regardless of tenant. All four now accept an optionalfilters: { organizationId?, projectId? }argument that is enforced on every adapter (inmemory, libsql, pg, mysql, mongodb, spanner):- On tenancy mismatch,
get*returnsnullat the storage layer. - On tenancy mismatch,
delete*is a silent no-op. - The tenancy predicate is folded into the destructive DML itself (scoped
WHEREon the DELETE, an atomic gate + delete inside a transaction, or a scoped subquery for the results cascade). A concurrent tenant swap of the same id between a pre-check and the DELETE cannot let a scoped delete hit another tenant's row.
Both behaviors match how a missing id already responds, so existence does not leak through error timing or messages.
The same atomic-DML pattern is also applied to
DatasetsStorage.deleteDatasetacross all 5 store adapters, closing a TOCTOU window between the pre-check and the parent DELETE that was introduced when tenancy filters were originally added.Dataset.getExperimentand the shared experiment-ownership gate onDatasetnow forward the dataset's tenancy scope to storage, so experiment reads and downstream mutations (list results, update result, delete experiment) reached through a dataset handle are automatically scoped to the owning tenant.Legacy calls that omit
filtersare unchanged, so this is fully backwards-compatible.// Before: any caller who knew the id could read/delete across tenants. await store.experiments.getExperimentById({ id: experimentId }); await store.experiments.deleteExperiment({ id: experimentId }); // After: pass the caller's scope; wrong tenant gets null / silent no-op. await store.experiments.getExperimentById({ id: experimentId, filters: { organizationId, projectId }, }); await store.experiments.deleteExperiment({ id: experimentId, filters: { organizationId, projectId }, });
- On tenancy mismatch,
-
Filled a pre-existing CRUD gap so the new dataset filter API works end-to-end on MySQL. (#18710)
createDataset,updateDataset, andmapDatasetnow persist and hydratetargetType,targetIds,scorerIds,tags, andrequestContextSchema. The columns were already declared by the shared schema but were never written or read, solistDatasets({ filters: { targetType, targetIds, name } })would have matched nothing on MySQL before this fix.alterTable.ifNotExistswas widened so in-place upgrades pick up the columns for older databases.Also fixed a
mapItemrow deserialization bug: when the stored input/groundTruth/metadata was a JSON string scalar, the mysql2 driver auto-parses the JSON column to a JS string and the previousparseJSONhelper then tried toJSON.parseit again and silently returnedundefined. It now falls back to the raw string when re-parsing fails, so versionedlistItems({ search })results round-trip the original input. -
Fixed a cross-tenant data-access issue on datasets by scoping
DatasetsManager.getandDatasetsManager.deleteto tenancy filters. (#18750)Previously
get({ id })anddelete({ id })looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of whichorganizationId/projectIdit belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).What changed
DatasetsManager.getandDatasetsManager.deleteaccept optionalorganizationIdandprojectId.- The tenancy is stashed on the returned
Datasethandle and forwarded to every downstream storage call (getDetails,update,addItem, item batch ops,startExperimentAsync). - The abstract storage contract (
getDatasetById,deleteDataset) gained an optionalfilters?: DatasetTenancyFiltersarg. - Item-mutation inputs (
AddDatasetItemInput,UpdateDatasetItemInput,BatchInsertItemsInput,BatchDeleteItemsInput) andUpdateDatasetInputaccept optionalfiltersfor the internal existence check.
Behavior
- Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
- On tenancy mismatch,
getthrows NOT_FOUND (returns null at the storage layer) anddeleteis a silent no-op — matching how a missing id already behaves, so existence does not leak through error timing or messages.
Example
// Before const ds = await mastra.datasets.get({ id }); await mastra.datasets.delete({ id }); // After — scope to a tenant const ds = await mastra.datasets.get({ id, organizationId, projectId }); await mastra.datasets.delete({ id, organizationId, projectId });
-
Add optional
batchId,datasetId, anddatasetItemIdfields to persisted scores so saved baseline scores can be grouped as one scoring pass and joined back to the dataset items they came from. (#18331)scoreTrace()accepts top-levelbatchId,datasetId, anddatasetItemIdwhen persisting a score for a stored trace.ScoreRowDataand score save payloads now include nullablebatchId,datasetId, anddatasetItemId.- Built-in stores with explicit score schema or attribute mappings now persist these provenance fields on saved scores.
- D1, DSQL, MSSQL, and Upstash score stores now apply additive provenance migrations or deterministic score ordering for persisted score reads.
await scoreTrace({ storage, scorer, target: { traceId }, batchId: 'baseline-batch-1', datasetId, datasetItemId, });
-
Added optional
organizationIdandprojectIdfields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and thelistScoresBy*methods accept afiltersoption to scope results by organization and project. (#18331)await storage.saveScore({ ...score, organizationId: 'org-a', projectId: 'proj-1' }); const result = await storage.listScoresByScorerId({ scorerId, filters: { organizationId: 'org-a', projectId: 'proj-1' }, });
projectIdidentifies the project scope, separate fromresourceIdwhich continues to mean the agent memory resource. -
Raise
@mastra/corepeer floor to>=1.49.0-0on all storage adapters so the tenancy-related named exports the adapters now consume are guaranteed to exist at install time. (#18861) -
Scoped
getDatasetByIdanddeleteDatasetto tenancy filters when the caller passesorganizationId/projectId. (#18750)The adapters now push the tenancy predicate into the SQL/query when the new optional
filtersargument is present. Legacy calls that omit tenancy are unchanged. On mismatch,getDatasetByIdreturnsnullanddeleteDatasetis a silent no-op — the cascade delete (dataset items and versions) is gated by a scoped parent pre-check, so cross-tenant data is never touched. -
Added optional
organizationIdandprojectIdquery parameters to the dataset routes. (#18750)GET /datasets/:datasetId,PATCH /datasets/:datasetId, andDELETE /datasets/:datasetIdnow accept optional tenancy query parameters. When provided, they are forwarded tomastra.datasets.get/.deleteand the operation returns 404 if the dataset does not belong to the requested tenant. Requests that omit the query parameters keep their existing behavior.Example
GET /datasets/abc123?organizationId=org_a&projectId=proj_1 DELETE /datasets/abc123?organizationId=org_a
@mastra/nestjs@0.2.4
Patch Changes
- Fixed Studio client-type detection to read the x-mastra-client-type header via request.headers instead of the Express-only request.get(), matching how the adapter reads headers everywhere else. (#18793)
@mastra/pg@1.15.0
Minor Changes
-
Added storage retention support to PostgreSQL. When you set a
retentionconfig,PostgresStorecan prune old rows from every growth-table domain it implements:memory(threads, messages, resources bycreatedAtZ),observability(spans bystartedAtZ),scores(bycreatedAtZ),workflows(run snapshots byupdatedAtZ),backgroundTasks(bycompletedAtZ, so in-flight tasks are never pruned),experiments(whole runs bycompletedAtZ, results cascade with their parent),notifications(bycreatedAtZ), andschedulesfire history (byactual_fire_at). (#18733)Deletes run in batches via
ctidsubqueries (bounded, resumable, and cancellable) so they stay safe on large tables. Anchor-column indexes are created lazily on the firstprune()call — never at init — so deployments that don't configure retention pay no extra index overhead.prune()only deletes rows; PostgreSQL's autovacuum reclaims the dead tuples for reuse.The v-next observability domain (day-partitioned signal event tables:
spans,metrics,logs,scores,feedback) is also covered:prune()drops whole day partitions — TimescaleDB chunks viadrop_chunks(), pg_partman children and native partitions via detach + drop — that are entirely older than the cutoff, so aging out event data is a metadata operation instead of a row-by-row delete.const storage = new PostgresStore({ id: 'mastra-storage', connectionString: process.env.DATABASE_URL, retention: { memory: { messages: { maxAge: '30d' } }, observability: { spans: { maxAge: '7d' } }, }, }); await storage.prune();
Patch Changes
-
Added optional tenancy arguments to
getDataset,updateDataset, anddeleteDataset. (#18750)You can now pass
organizationIdandprojectIdto scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throwDATASET_NOT_FOUND(surfaced as a 404 over HTTP). Deletes silently no-op on a tenancy mismatch — matching the existing "delete non-existent id is a no-op" semantics so cross-tenant existence is never leaked via error timing or status.Example
// Before await client.getDataset('abc123'); await client.deleteDataset('abc123'); await client.updateDataset({ id: 'abc123', name: 'renamed' }); // After — scope to a tenant await client.getDataset('abc123', { organizationId: 'org_a', projectId: 'proj_1' }); await client.deleteDataset('abc123', { organizationId: 'org_a' }); await client.updateDataset({ id: 'abc123', name: 'renamed', organizationId: 'org_a' });
-
Pushed remaining dataset read filters and pagination down to storage. (#18710)
DatasetsManager.list({ filters })now acceptstargetType,targetIds(overlap/union semantics), andname(substring, case-insensitive) in addition to the existing tenancy and candidate filters. Filtering is pushed down to the storage layer so callers no longer have to post-filter results.Storage adapters must also be upgraded to the versions listed below to honor the new filters. If a caller is on this version of
@mastra/corebut on an older storage adapter, the newtargetType/targetIds/namefilter keys are silently ignored by the adapter — no runtime error, but the filter has no effect and every dataset in the tenancy is returned.Dataset.listItems({ version, search, page, perPage })now appliessearchand pagination at the storage layer whenversionis provided alongside any of those. Previously they were silently dropped wheneverversionwas set. The return shape is unchanged: passing onlyversionstill returns a bareDatasetItem[]snapshot; passingsearch,page, orperPage(with or withoutversion) returns the paginated{ items, pagination }shape. The bare-array branch is marked@deprecated; prefer passingpage/perPageto always receive the paginated shape. -
Tenancy-scope experiments
getByIdanddelete*onExperimentsStorage. (#18770)ExperimentsStorage.getExperimentById,getExperimentResultById,deleteExperiment, anddeleteExperimentResultsused to key on the primary id alone, so any caller who knew the id could read or delete the row regardless of tenant. All four now accept an optionalfilters: { organizationId?, projectId? }argument that is enforced on every adapter (inmemory, libsql, pg, mysql, mongodb, spanner):- On tenancy mismatch,
get*returnsnullat the storage layer. - On tenancy mismatch,
delete*is a silent no-op. - The tenancy predicate is folded into the destructive DML itself (scoped
WHEREon the DELETE, an atomic gate + delete inside a transaction, or a scoped subquery for the results cascade). A concurrent tenant swap of the same id between a pre-check and the DELETE cannot let a scoped delete hit another tenant's row.
Both behaviors match how a missing id already responds, so existence does not leak through error timing or messages.
The same atomic-DML pattern is also applied to
DatasetsStorage.deleteDatasetacross all 5 store adapters, closing a TOCTOU window between the pre-check and the parent DELETE that was introduced when tenancy filters were originally added.Dataset.getExperimentand the shared experiment-ownership gate onDatasetnow forward the dataset's tenancy scope to storage, so experiment reads and downstream mutations (list results, update result, delete experiment) reached through a dataset handle are automatically scoped to the owning tenant.Legacy calls that omit
filtersare unchanged, so this is fully backwards-compatible.// Before: any caller who knew the id could read/delete across tenants. await store.experiments.getExperimentById({ id: experimentId }); await store.experiments.deleteExperiment({ id: experimentId }); // After: pass the caller's scope; wrong tenant gets null / silent no-op. await store.experiments.getExperimentById({ id: experimentId, filters: { organizationId, projectId }, }); await store.experiments.deleteExperiment({ id: experimentId, filters: { organizationId, projectId }, });
- On tenancy mismatch,
-
Fixed a double-encoding bug where
createDatasetandupdateDatasetstoredtargetIdsandscorerIdsas JSON-encoded strings into theJSONBcolumns instead of arrays. This caused the newlistDatasets({ filters: { targetIds } })overlap query (targetIds ?| array[...]) to never match. (#18710)Existing rows written before this fix are still double-encoded and will not be matched by the new
targetIdsfilter. They self-heal on the nextupdateDatasetcall. Deployments with a long tail of pre-existing datasets should run a one-time backfill (re-encodingtargetIdsandscorerIdson affected rows) rather than rely on incidental writes; this can be tracked as a follow-up if needed. -
Fixed a cross-tenant data-access issue on datasets by scoping
DatasetsManager.getandDatasetsManager.deleteto tenancy filters. (#18750)Previously
get({ id })anddelete({ id })looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of whichorganizationId/projectIdit belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).What changed
DatasetsManager.getandDatasetsManager.deleteaccept optionalorganizationIdandprojectId.- The tenancy is stashed on the returned
Datasethandle and forwarded to every downstream storage call (getDetails,update,addItem, item batch ops,startExperimentAsync). - The abstract storage contract (
getDatasetById,deleteDataset) gained an optionalfilters?: DatasetTenancyFiltersarg. - Item-mutation inputs (
AddDatasetItemInput,UpdateDatasetItemInput,BatchInsertItemsInput,BatchDeleteItemsInput) andUpdateDatasetInputaccept optionalfiltersfor the internal existence check.
Behavior
- Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
- On tenancy mismatch,
getthrows NOT_FOUND (returns null at the storage layer) anddeleteis a silent no-op — matching how a missing id already behaves, so existence does not leak through error timing or messages.
Example
// Before const ds = await mastra.datasets.get({ id }); await mastra.datasets.delete({ id }); // After — scope to a tenant const ds = await mastra.datasets.get({ id, organizationId, projectId }); await mastra.datasets.delete({ id, organizationId, projectId });
-
Add optional
batchId,datasetId, anddatasetItemIdfields to persisted scores so saved baseline scores can be grouped as one scoring pass and joined back to the dataset items they came from. (#18331)scoreTrace()accepts top-levelbatchId,datasetId, anddatasetItemIdwhen persisting a score for a stored trace.ScoreRowDataand score save payloads now include nullablebatchId,datasetId, anddatasetItemId.- Built-in stores with explicit score schema or attribute mappings now persist these provenance fields on saved scores.
- D1, DSQL, MSSQL, and Upstash score stores now apply additive provenance migrations or deterministic score ordering for persisted score reads.
await scoreTrace({ storage, scorer, target: { traceId }, batchId: 'baseline-batch-1', datasetId, datasetItemId, });
-
Added multi-tenant scoping to stored scorer definitions. Stored scorers now persist optional
organizationIdandprojectIdon the definition record, andlist/listResolvedaccept matching filters to scope results by tenant. The Postgres adapter backfills the new columns and applies the scoped filters; tenancy lives on the record while version snapshots stay pure config. (#18331)await storage.create({ scorerDefinition: { id, organizationId: 'org-a', projectId: 'proj-1', ...config }, }); const { scorerDefinitions } = await storage.list({ status: 'draft', organizationId: 'org-a', projectId: 'proj-1', });
-
Added optional
organizationIdandprojectIdfields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and thelistScoresBy*methods accept afiltersoption to scope results by organization and project. (#18331)await storage.saveScore({ ...score, organizationId: 'org-a', projectId: 'proj-1' }); const result = await storage.listScoresByScorerId({ scorerId, filters: { organizationId: 'org-a', projectId: 'proj-1' }, });
projectIdidentifies the project scope, separate fromresourceIdwhich continues to mean the agent memory resource. -
Raise
@mastra/corepeer floor to>=1.49.0-0on all storage adapters so the tenancy-related named exports the adapters now consume are guaranteed to exist at install time. (#18861) -
Scoped
getDatasetByIdanddeleteDatasetto tenancy filters when the caller passesorganizationId/projectId. (#18750)The adapters now push the tenancy predicate into the SQL/query when the new optional
filtersargument is present. Legacy calls that omit tenancy are unchanged. On mismatch,getDatasetByIdreturnsnullanddeleteDatasetis a silent no-op — the cascade delete (dataset items and versions) is gated by a scoped parent pre-check, so cross-tenant data is never touched. -
Added optional
organizationIdandprojectIdquery parameters to the dataset routes. (#18750)GET /datasets/:datasetId,PATCH /datasets/:datasetId, andDELETE /datasets/:datasetIdnow accept optional tenancy query parameters. When provided, they are forwarded tomastra.datasets.get/.deleteand the operation returns 404 if the dataset does not belong to the requested tenant. Requests that omit the query parameters keep their existing behavior.Example
GET /datasets/abc123?organizationId=org_a&projectId=proj_1 DELETE /datasets/abc123?organizationId=org_a
@mastra/playground-ui@39.0.0
Minor Changes
-
Removed named exports from the
@mastra/playground-uiroot entry. Import public APIs from exact package subpaths instead. (#18791)Before
import { Button } from '@mastra/playground-ui';
After
import { Button } from '@mastra/playground-ui/components/Button';
mastracodenow uses the exact subpath imports, and lint rules prevent new broad@mastra/playground-uiimports. -
Removed the
Searchbarcomponent from@mastra/playground-ui. Compose search inputs withInputGroupinstead so search remains a documented use case of the existing input composition primitive. (#18727)Before
import { Searchbar } from '@mastra/playground-ui/components/Searchbar'; <Searchbar label="Search tools" placeholder="Search tools..." onSearch={setSearch} />;
After
import { InputGroup, InputGroupAddon, InputGroupInput } from '@mastra/playground-ui/components/InputGroup'; import { SearchIcon } from 'lucide-react'; <InputGroup variant="outline"> <InputGroupAddon align="inline-start"> <SearchIcon /> </InputGroupAddon> <InputGroupInput type="search" aria-label="Search tools" placeholder="Search tools..." onChange={event => setSearch(event.target.value)} /> </InputGroup>;
-
Signals now show live Entity-Learning data (#18699)
The Signals page is no longer static. Select an agent reported by the platform and Signals fetches that agent's signals and their clusters live from the Entity-Learning API, replacing the previous hardcoded mock data. Each available signal loads its real clusters (topics) and traces, with a scatter-plot chart for the selected topics.
What changed
- Added an agent filter at the top of the Signals page, mirroring the traces filter, so you can inspect signals for any agent on the server.
- The Signals overview and details pages now render live Entity-Learning topics, examples, and points directly, with shape-matching skeletons while data loads, centered empty states, and explicit error states.
- Clicking a cluster card opens its topic by default, and the Signals breadcrumbs preserve the selected entity and topic query params on back-navigation.
- Signals detail navigation keeps selected clusters, trace examples, and chart filters in sync when moving between signals or entities.
Gating
Studio's served HTML exposes
MASTRA_ORGANIZATION_ID,MASTRA_PLATFORM_PROJECT_ID, andMASTRA_PLATFORM_OBSERVABILITY_ENDPOINTto the browser so the Signals page can call the Entity-Learning API. The route is gated on the platform observability config, and theMASTRA_SIGNALS_UIflag guards the sidebar Signals nav link.
Patch Changes
-
Fixed the Signals page showing empty clusters for every signal except the most recently clustered one. Cluster queries no longer pin the entity-wide latest run id: the API resolves the latest run per signal, and the details page reuses the run resolved by the topics response for its examples and points queries. (#18786)
-
Fix the Signals (Agent Learning) client to call the platform query service's session-authenticated
/api/learning/*routes instead of the internal/entity-learning/*output-service contract. The client now derives the query-service origin from the injected observability endpoint, sends the WorkOS session cookie viacredentials: 'include', and scopes reads with theX-Mastra-Project-Idheader — matching the existing/api/observability/*auth pattern. Previously the Signals UI called an internal-only endpoint with no credentials, which 404'd on every hosted and local deployment. (#18852)
@mastra/railway@0.3.0
Minor Changes
-
Added checkpoint-backed restart and reconnect support to
RailwaySandbox. PasscheckpointNameto save the sandbox filesystem before idle teardown and restore it when a sandbox must be recreated. (#18725)const sandbox = new RailwaySandbox({ checkpointName: 'mastra-workspace-cache', idleTimeoutMinutes: 30, });
Added
restart()and automatic retry for unavailable Railway sandboxes during command execution. Fixed checkpoint refresh scheduling, restart checkpoint flushing, teardown cleanup, and retry classification so checkpoint state is preserved without replaying commands after ambiguous transport failures.
Patch Changes
@mastra/redis@1.2.2
Patch Changes
-
Added optional
organizationIdandprojectIdfields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and thelistScoresBy*methods accept afiltersoption to scope results by organization and project. (#18331)await storage.saveScore({ ...score, organizationId: 'org-a', projectId: 'proj-1' }); const result = await storage.listScoresByScorerId({ scorerId, filters: { organizationId: 'org-a', projectId: 'proj-1' }, });
projectIdidentifies the project scope, separate fromresourceIdwhich continues to mean the agent memory resource.
@mastra/schema-compat@1.3.3
Patch Changes
- Fix the Zod v4 nullable and optional handlers gating on the wrapper type instead of the wrapped inner type. They checked
value.constructor.name(always"ZodNullable"/"ZodOptional"), so the inner type was always processed. A nullable/optional wrapping an unsupported inner type (such as a tuple) is now passed through unchanged, matching the v3 handler, instead of being processed and rejected. Closes #18687. (#18688)
@mastra/server@1.49.0
Minor Changes
-
Added optional
organizationIdandprojectIdquery parameters to the dataset routes. (#18750)GET /datasets/:datasetId,PATCH /datasets/:datasetId, andDELETE /datasets/:datasetIdnow accept optional tenancy query parameters. When provided, they are forwarded tomastra.datasets.get/.deleteand the operation returns 404 if the dataset does not belong to the requested tenant. Requests that omit the query parameters keep their existing behavior.Example
GET /datasets/abc123?organizationId=org_a&projectId=proj_1 DELETE /datasets/abc123?organizationId=org_a
Patch Changes
-
Added optional tenancy arguments to
getDataset,updateDataset, anddeleteDataset. (#18750)You can now pass
organizationIdandprojectIdto scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throwDATASET_NOT_FOUND(surfaced as a 404 over HTTP). Deletes silently no-op on a tenancy mismatch — matching the existing "delete non-existent id is a no-op" semantics so cross-tenant existence is never leaked via error timing or status.Example
// Before await client.getDataset('abc123'); await client.deleteDataset('abc123'); await client.updateDataset({ id: 'abc123', name: 'renamed' }); // After — scope to a tenant await client.getDataset('abc123', { organizationId: 'org_a', projectId: 'proj_1' }); await client.deleteDataset('abc123', { organizationId: 'org_a' }); await client.updateDataset({ id: 'abc123', name: 'renamed', organizationId: 'org_a' });
-
Fixed a TypeScript error where auth provider instances (for example
new MastraAuthWorkos()) could not be assigned toserver.authorstudio.auth, failing withProperty '#private' is missing(#18682). (#18796)Auth providers are now typed with a new structural
IMastraAuthProviderinterface (exported from@mastra/core/serverand@mastra/auth), so provider packages no longer need a shared class identity with@mastra/core.CompositeAuthalso accepts anyIMastraAuthProviderimplementation. No code changes are required:import { Mastra } from '@mastra/core'; import { MastraAuthWorkos } from '@mastra/auth-workos'; // Previously failed to compile with TS2322, now works without casts export const mastra = new Mastra({ server: { auth: new MastraAuthWorkos(), }, });
-
Fixed a cross-tenant data-access issue on datasets by scoping
DatasetsManager.getandDatasetsManager.deleteto tenancy filters. (#18750)Previously
get({ id })anddelete({ id })looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of whichorganizationId/projectIdit belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).What changed
DatasetsManager.getandDatasetsManager.deleteaccept optionalorganizationIdandprojectId.- The tenancy is stashed on the returned
Datasethandle and forwarded to every downstream storage call (getDetails,update,addItem, item batch ops,startExperimentAsync). - The abstract storage contract (
getDatasetById,deleteDataset) gained an optionalfilters?: DatasetTenancyFiltersarg. - Item-mutation inputs (
AddDatasetItemInput,UpdateDatasetItemInput,BatchInsertItemsInput,BatchDeleteItemsInput) andUpdateDatasetInputaccept optionalfiltersfor the internal existence check.
Behavior
- Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
- On tenancy mismatch,
getthrows NOT_FOUND (returns null at the storage layer) anddeleteis a silent no-op — matching how a missing id already behaves, so existence does not leak through error timing or messages.
Example
// Before const ds = await mastra.datasets.get({ id }); await mastra.datasets.delete({ id }); // After — scope to a tenant const ds = await mastra.datasets.get({ id, organizationId, projectId }); await mastra.datasets.delete({ id, organizationId, projectId });
-
Scoped
getDatasetByIdanddeleteDatasetto tenancy filters when the caller passesorganizationId/projectId. (#18750)The adapters now push the tenancy predicate into the SQL/query when the new optional
filtersargument is present. Legacy calls that omit tenancy are unchanged. On mismatch,getDatasetByIdreturnsnullanddeleteDatasetis a silent no-op — the cascade delete (dataset items and versions) is gated by a scoped parent pre-check, so cross-tenant data is never touched.
@mastra/spanner@1.2.2
Patch Changes
-
Added optional tenancy arguments to
getDataset,updateDataset, anddeleteDataset. (#18750)You can now pass
organizationIdandprojectIdto scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throwDATASET_NOT_FOUND(surfaced as a 404 over HTTP). Deletes silently no-op on a tenancy mismatch — matching the existing "delete non-existent id is a no-op" semantics so cross-tenant existence is never leaked via error timing or status.Example
// Before await client.getDataset('abc123'); await client.deleteDataset('abc123'); await client.updateDataset({ id: 'abc123', name: 'renamed' }); // After — scope to a tenant await client.getDataset('abc123', { organizationId: 'org_a', projectId: 'proj_1' }); await client.deleteDataset('abc123', { organizationId: 'org_a' }); await client.updateDataset({ id: 'abc123', name: 'renamed', organizationId: 'org_a' });
-
Pushed remaining dataset read filters and pagination down to storage. (#18710)
DatasetsManager.list({ filters })now acceptstargetType,targetIds(overlap/union semantics), andname(substring, case-insensitive) in addition to the existing tenancy and candidate filters. Filtering is pushed down to the storage layer so callers no longer have to post-filter results.Storage adapters must also be upgraded to the versions listed below to honor the new filters. If a caller is on this version of
@mastra/corebut on an older storage adapter, the newtargetType/targetIds/namefilter keys are silently ignored by the adapter — no runtime error, but the filter has no effect and every dataset in the tenancy is returned.Dataset.listItems({ version, search, page, perPage })now appliessearchand pagination at the storage layer whenversionis provided alongside any of those. Previously they were silently dropped wheneverversionwas set. The return shape is unchanged: passing onlyversionstill returns a bareDatasetItem[]snapshot; passingsearch,page, orperPage(with or withoutversion) returns the paginated{ items, pagination }shape. The bare-array branch is marked@deprecated; prefer passingpage/perPageto always receive the paginated shape. -
Tenancy-scope experiments
getByIdanddelete*onExperimentsStorage. (#18770)ExperimentsStorage.getExperimentById,getExperimentResultById,deleteExperiment, anddeleteExperimentResultsused to key on the primary id alone, so any caller who knew the id could read or delete the row regardless of tenant. All four now accept an optionalfilters: { organizationId?, projectId? }argument that is enforced on every adapter (inmemory, libsql, pg, mysql, mongodb, spanner):- On tenancy mismatch,
get*returnsnullat the storage layer. - On tenancy mismatch,
delete*is a silent no-op. - The tenancy predicate is folded into the destructive DML itself (scoped
WHEREon the DELETE, an atomic gate + delete inside a transaction, or a scoped subquery for the results cascade). A concurrent tenant swap of the same id between a pre-check and the DELETE cannot let a scoped delete hit another tenant's row.
Both behaviors match how a missing id already responds, so existence does not leak through error timing or messages.
The same atomic-DML pattern is also applied to
DatasetsStorage.deleteDatasetacross all 5 store adapters, closing a TOCTOU window between the pre-check and the parent DELETE that was introduced when tenancy filters were originally added.Dataset.getExperimentand the shared experiment-ownership gate onDatasetnow forward the dataset's tenancy scope to storage, so experiment reads and downstream mutations (list results, update result, delete experiment) reached through a dataset handle are automatically scoped to the owning tenant.Legacy calls that omit
filtersare unchanged, so this is fully backwards-compatible.// Before: any caller who knew the id could read/delete across tenants. await store.experiments.getExperimentById({ id: experimentId }); await store.experiments.deleteExperiment({ id: experimentId }); // After: pass the caller's scope; wrong tenant gets null / silent no-op. await store.experiments.getExperimentById({ id: experimentId, filters: { organizationId, projectId }, }); await store.experiments.deleteExperiment({ id: experimentId, filters: { organizationId, projectId }, });
- On tenancy mismatch,
-
Fixed a cross-tenant data-access issue on datasets by scoping
DatasetsManager.getandDatasetsManager.deleteto tenancy filters. (#18750)Previously
get({ id })anddelete({ id })looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of whichorganizationId/projectIdit belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).What changed
DatasetsManager.getandDatasetsManager.deleteaccept optionalorganizationIdandprojectId.- The tenancy is stashed on the returned
Datasethandle and forwarded to every downstream storage call (getDetails,update,addItem, item batch ops,startExperimentAsync). - The abstract storage contract (
getDatasetById,deleteDataset) gained an optionalfilters?: DatasetTenancyFiltersarg. - Item-mutation inputs (
AddDatasetItemInput,UpdateDatasetItemInput,BatchInsertItemsInput,BatchDeleteItemsInput) andUpdateDatasetInputaccept optionalfiltersfor the internal existence check.
Behavior
- Omitting