Skip to content

July 3, 2026

Latest

Choose a tag to compare

@PaulieScanlon PaulieScanlon released this 07 Jul 07:59

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-ui removed root named exports; import from explicit subpaths (e.g. @mastra/playground-ui/components/Button).
  • @mastra/playground-ui removed Searchbar; use InputGroup composition instead.

Changelog

@mastra/core@1.49.0

Minor Changes

  • Added opt-in storage retention. Declare per-table maxAge policies in the retention config, then call storage.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), and schedules (fire history). Anchors are chosen so maxAge is 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 reports done: 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.get and DatasetsManager.delete to tenancy filters. (#18750)

    Previously get({ id }) and delete({ id }) looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of which organizationId / projectId it belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).

    What changed

    • DatasetsManager.get and DatasetsManager.delete accept optional organizationId and projectId.
    • The tenancy is stashed on the returned Dataset handle and forwarded to every downstream storage call (getDetails, update, addItem, item batch ops, startExperimentAsync).
    • The abstract storage contract (getDatasetById, deleteDataset) gained an optional filters?: DatasetTenancyFilters arg.
    • Item-mutation inputs (AddDatasetItemInput, UpdateDatasetItemInput, BatchInsertItemsInput, BatchDeleteItemsInput) and UpdateDatasetInput accept optional filters for the internal existence check.

    Behavior

    • Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
    • On tenancy mismatch, get throws NOT_FOUND (returns null at the storage layer) and delete is 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() and scoreTraceBatch() to @mastra/core/evals/scoreTraces for scoring stored traces without re-running the agent. (#18331)

    • scoreTrace() can score either a stored trace reference or a preloaded TraceRecord, and it returns the persisted ScoreRowData after 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, and datasetItemId fields 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-level batchId, datasetId, and datasetItemId when persisting a score for a stored trace.
    • ScoreRowData and score save payloads now include nullable batchId, datasetId, and datasetItemId.
    • 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 organizationId and projectId on the definition record, and list/listResolved accept 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 organizationId and projectId fields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and the listScoresBy* methods accept a filters option 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' },
    });

    projectId identifies the project scope, separate from resourceId which continues to mean the agent memory resource.

  • Agents using models that dropped support for temperature, topP, or topK (such as claude-opus-4-7 or gpt-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 addExperimentResult threw during runExperiment, the failure was silently logged with console.warn and the run continued. The item was still counted as succeeded or failed based on the agent run outcome, so ExperimentSummary.succeededCount could report more rows than actually existed in mastra_experiment_results — silent data loss with no signal to the caller. (#18716)

    Now each item result carries an optional persistenceError: { message } | null field, and the summary exposes an optional persistenceFailures: number counter. 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 inspect persistenceFailures to 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 of console.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 / 0 on 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 start chunk for correct stream ordering, handle TripWire from input processors during preparation, and port onInputAvailable/onOutput tool lifecycle hooks to the durable tool execution path. Removed stale test harness guards that were preventing isTaskComplete, actor, savePerStep, and providerOptions from 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() and Dataset.listExperimentResults(). The storage layer already accepted these filters — they are now reachable from the Dataset handle. 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,
    });

    listExperiments accepts: targetType, targetId, agentVersion, status, tenancy filters.
    listExperimentResults accepts: traceId, status, tenancy filters.

    Enables baseline vs variant read patterns without client-side filtering or bypassing Dataset.

  • Added optional tenancy arguments to getDataset, updateDataset, and deleteDataset. (#18750)

    You can now pass organizationId and projectId to scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throw DATASET_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 accepts targetType, targetIds (overlap/union semantics), and name (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/core but on an older storage adapter, the new targetType/targetIds/name filter 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 applies search and pagination at the storage layer when version is provided alongside any of those. Previously they were silently dropped whenever version was set. The return shape is unchanged: passing only version still returns a bare DatasetItem[] snapshot; passing search, page, or perPage (with or without version) returns the paginated { items, pagination } shape. The bare-array branch is marked @deprecated; prefer passing page / perPage to always receive the paginated shape.

  • Fixed SystemPromptScrubber processOutputStream swallowing TripWire errors when strategy is block. The abort call now correctly propagates the TripWire to halt the agent stream, matching the existing behavior in processOutputResult. (#18794)

  • Fixed five DurableAgent behavioral parity gaps with the regular Agent loop: (#18712)

    • Goal step: Durable agents now honor goal-aware stop semantics. The goalStep has 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 processLLMRequest now work on durable agents, short-circuiting the model call and replaying cached chunks.
    • toModelOutput normalization: Durable agents now call toModelOutput on 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: onInputStart and onInputDelta callbacks on tool definitions are now invoked during durable agent streaming, and client-tool observability spans are created for tool input streaming.
  • DurableAgent now matches Agent for 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 autoResumeSuspendedTools is enabled.
    • Agents wired to a BackgroundTaskManager get 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.
    • prepareStep and input-processor overrides — including model, tools, activeTools, providerOptions, modelSettings, structuredOutput, and workspace — apply identically on both paths.
  • Durable agents now honor onIterationComplete callback return values and delegation bail signals in the loop predicate, closing three behavioral parity gaps with the regular agent: (#18707)

    • Delegation bail — When an onDelegationComplete hook calls ctx.bail(), the durable loop now stops at the next predicate evaluation instead of continuing indefinitely. The delegationBailed flag propagates through DurableAgenticExecutionOutput and baseIterationStateSchema.
    • onIterationComplete callback dispatch — The durable predicate now calls onIterationComplete directly (read from globalRunRegistry) and honors its return value: { continue: false } stops the loop, { continue: true } forces continuation when maxSteps allows, and { feedback } injects a user message for the next LLM turn.
    • Two-phase stop (pendingFeedbackStop)onIterationComplete returning { continue: false, feedback: '...' } now schedules exactly one more LLM turn before stopping, matching the regular agent's behavior. The pendingFeedbackStop flag is persisted in baseIterationStateSchema across iterations.

    Signal drain (bugs 5 and 11) is deferred — DurableAgent does not yet participate in agentThreadStreamRuntime and has no sendMessage / signal infrastructure.

    Scenario tests delegation-complete-bail and stop-condition-long-loop now run on the durable engine. The aimock-scenario harness no longer drops stopWhen, delegation, or onIterationComplete for durable runs.

  • 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's toModelOutput the placeholder string from tool-call-step.ts ("Background task started...") instead of the agentOutputSchema object. toModelOutput read output.text, which is undefined for that string, so the supervisor's next request carried a role: "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 } }).

    toModelOutput now 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)

  • createCodingAgent now only includes the default TaskSignalProvider when memory is configured. Previously it always wired TaskSignalProvider, whose TaskStateProcessor requires 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 requireApproval tool 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' with approval: { id, approved: false, reason }, so recalled AI SDK v6 UI parts render as output-denied. In v4 and v5 (which have no denied state) the call downgrades to a single output-available (v5) / result (v4) part whose output is the decline reason — so the agent's onFinish memory save no longer throws ToolInvocation 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.

  • Fixed reasoning text being lost in AIV4Adapter and stream-chunk assembly at the end of the turn (#18534)

  • Tenancy-scope experiments getById and delete* on ExperimentsStorage. (#18770)

    ExperimentsStorage.getExperimentById, getExperimentResultById, deleteExperiment, and deleteExperimentResults used 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 optional filters: { organizationId?, projectId? } argument that is enforced on every adapter (inmemory, libsql, pg, mysql, mongodb, spanner):

    • On tenancy mismatch, get* returns null at the storage layer.
    • On tenancy mismatch, delete* is a silent no-op.
    • The tenancy predicate is folded into the destructive DML itself (scoped WHERE on 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.deleteDataset across 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.getExperiment and the shared experiment-ownership gate on Dataset now 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 filters are 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 },
    });
  • Fixed a TypeScript error where auth provider instances (for example new MastraAuthWorkos()) could not be assigned to server.auth or studio.auth, failing with Property '#private' is missing (#18682). (#18796)

    Auth providers are now typed with a new structural IMastraAuthProvider interface (exported from @mastra/core/server and @mastra/auth), so provider packages no longer need a shared class identity with @mastra/core. CompositeAuth also accepts any IMastraAuthProvider implementation. 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 getDatasetById and deleteDataset to tenancy filters when the caller passes organizationId / projectId. (#18750)

    The adapters now push the tenancy predicate into the SQL/query when the new optional filters argument is present. Legacy calls that omit tenancy are unchanged. On mismatch, getDatasetById returns null and deleteDataset is 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 organizationId and projectId query parameters to the dataset routes. (#18750)

    GET /datasets/:datasetId, PATCH /datasets/:datasetId, and DELETE /datasets/:datasetId now accept optional tenancy query parameters. When provided, they are forwarded to mastra.datasets.get / .delete and 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.headers to durable storage. Headers (which may contain sensitive API keys or auth tokens) are now stripped during serialization and kept in-process on the RunRegistryEntry, 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 to server.auth or studio.auth, failing with Property '#private' is missing (#18682). (#18796)

    Auth providers are now typed with a new structural IMastraAuthProvider interface (exported from @mastra/core/server and @mastra/auth), so provider packages no longer need a shared class identity with @mastra/core. CompositeAuth also accepts any IMastraAuthProvider implementation. 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 organizationId and projectId fields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and the listScoresBy* methods accept a filters option 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' },
    });

    projectId identifies the project scope, separate from resourceId which continues to mean the agent memory resource.

@mastra/client-js@1.30.0

Minor Changes

  • Added optional tenancy arguments to getDataset, updateDataset, and deleteDataset. (#18750)

    You can now pass organizationId and projectId to scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throw DATASET_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.get and DatasetsManager.delete to tenancy filters. (#18750)

    Previously get({ id }) and delete({ id }) looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of which organizationId / projectId it belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).

    What changed

    • DatasetsManager.get and DatasetsManager.delete accept optional organizationId and projectId.
    • The tenancy is stashed on the returned Dataset handle and forwarded to every downstream storage call (getDetails, update, addItem, item batch ops, startExperimentAsync).
    • The abstract storage contract (getDatasetById, deleteDataset) gained an optional filters?: DatasetTenancyFilters arg.
    • Item-mutation inputs (AddDatasetItemInput, UpdateDatasetItemInput, BatchInsertItemsInput, BatchDeleteItemsInput) and UpdateDatasetInput accept optional filters for the internal existence check.

    Behavior

    • Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
    • On tenancy mismatch, get throws NOT_FOUND (returns null at the storage layer) and delete is 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 getDatasetById and deleteDataset to tenancy filters when the caller passes organizationId / projectId. (#18750)

    The adapters now push the tenancy predicate into the SQL/query when the new optional filters argument is present. Legacy calls that omit tenancy are unchanged. On mismatch, getDatasetById returns null and deleteDataset is 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 organizationId and projectId query parameters to the dataset routes. (#18750)

    GET /datasets/:datasetId, PATCH /datasets/:datasetId, and DELETE /datasets/:datasetId now accept optional tenancy query parameters. When provided, they are forwarded to mastra.datasets.get / .delete and 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 organizationId and projectId fields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and the listScoresBy* methods accept a filters option 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' },
    });

    projectId identifies the project scope, separate from resourceId which continues to mean the agent memory resource.

@mastra/cloudflare-d1@1.1.1

Patch Changes

  • Add optional batchId, datasetId, and datasetItemId fields 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-level batchId, datasetId, and datasetItemId when persisting a score for a stored trace.
    • ScoreRowData and score save payloads now include nullable batchId, datasetId, and datasetItemId.
    • 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 organizationId and projectId fields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and the listScoresBy* methods accept a filters option 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' },
    });

    projectId identifies the project scope, separate from resourceId which 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 codemod now works on project paths containing spaces. (#18804)

@mastra/convex@1.3.1

Patch Changes

  • Added optional organizationId and projectId fields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and the listScoresBy* methods accept a filters option 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' },
    });

    projectId identifies the project scope, separate from resourceId which 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 codemod now works on project paths containing spaces. (#18804)

  • Fix injectStudioHtmlConfig corrupting Studio config values that contain $. Values such as request context presets are now injected into the HTML verbatim instead of being mangled by String.prototype.replace special 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, and MASTRA_PLATFORM_OBSERVABILITY_ENDPOINT to the browser so the Signals page can call the Entity-Learning API. The route is gated on the platform observability config, and the MASTRA_SIGNALS_UI flag 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, and MASTRA_PLATFORM_OBSERVABILITY_ENDPOINT to the browser so the Signals page can call the Entity-Learning API. The route is gated on the platform observability config, and the MASTRA_SIGNALS_UI flag guards the sidebar Signals nav link.

@mastra/dsql@1.1.2

Patch Changes

  • Add optional batchId, datasetId, and datasetItemId fields 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-level batchId, datasetId, and datasetItemId when persisting a score for a stored trace.
    • ScoreRowData and score save payloads now include nullable batchId, datasetId, and datasetItemId.
    • 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 organizationId and projectId fields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and the listScoresBy* methods accept a filters option 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' },
    });

    projectId identifies the project scope, separate from resourceId which continues to mean the agent memory resource.

@mastra/dynamodb@1.1.2

Patch Changes

  • Add optional batchId, datasetId, and datasetItemId fields 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-level batchId, datasetId, and datasetItemId when persisting a score for a stored trace.
    • ScoreRowData and score save payloads now include nullable batchId, datasetId, and datasetItemId.
    • 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 organizationId and projectId fields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and the listScoresBy* methods accept a filters option 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' },
    });

    projectId identifies the project scope, separate from resourceId which continues to mean the agent memory resource.

@mastra/e2b@0.5.0

Minor Changes

Patch Changes

@mastra/hono@1.5.4

Patch Changes

@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, and datasetItemId fields 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-level batchId, datasetId, and datasetItemId when persisting a score for a stored trace.
    • ScoreRowData and score save payloads now include nullable batchId, datasetId, and datasetItemId.
    • 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 organizationId and projectId fields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and the listScoresBy* methods accept a filters option 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' },
    });

    projectId identifies the project scope, separate from resourceId which continues to mean the agent memory resource.

@mastra/libsql@1.15.0

Minor Changes

  • Added storage retention support to libSQL. When you set a retention config, LibSQLStore can prune old rows from every growth-table domain: memory (threads, messages, resources by createdAt), threadState (by updatedAt), observability (spans by startedAt), scores (by createdAt), workflows (run snapshots by updatedAt), backgroundTasks (by completedAt, so in-flight tasks are never pruned), experiments (whole runs by completedAt, results cascade with their parent), notifications and harness sessions (by createdAt), and schedules fire history (by actual_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 a VACUUM on 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, and deleteDataset. (#18750)

    You can now pass organizationId and projectId to scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throw DATASET_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 accepts targetType, targetIds (overlap/union semantics), and name (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/core but on an older storage adapter, the new targetType/targetIds/name filter 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 applies search and pagination at the storage layer when version is provided alongside any of those. Previously they were silently dropped whenever version was set. The return shape is unchanged: passing only version still returns a bare DatasetItem[] snapshot; passing search, page, or perPage (with or without version) returns the paginated { items, pagination } shape. The bare-array branch is marked @deprecated; prefer passing page / perPage to always receive the paginated shape.

  • Fixed a double-encoding bug where createDataset stored targetIds and scorerIds as JSON-encoded strings instead of arrays. This caused the new listDatasets({ 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 targetIds filter. They self-heal on the next updateDataset call. Deployments with a long tail of pre-existing datasets should run a one-time backfill (re-encoding targetIds and scorerIds on affected rows) rather than rely on incidental writes; this can be tracked as a follow-up if needed.

  • Tenancy-scope experiments getById and delete* on ExperimentsStorage. (#18770)

    ExperimentsStorage.getExperimentById, getExperimentResultById, deleteExperiment, and deleteExperimentResults used 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 optional filters: { organizationId?, projectId? } argument that is enforced on every adapter (inmemory, libsql, pg, mysql, mongodb, spanner):

    • On tenancy mismatch, get* returns null at the storage layer.
    • On tenancy mismatch, delete* is a silent no-op.
    • The tenancy predicate is folded into the destructive DML itself (scoped WHERE on 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.deleteDataset across 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.getExperiment and the shared experiment-ownership gate on Dataset now 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 filters are 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 },
    });
  • Fixed a cross-tenant data-access issue on datasets by scoping DatasetsManager.get and DatasetsManager.delete to tenancy filters. (#18750)

    Previously get({ id }) and delete({ id }) looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of which organizationId / projectId it belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).

    What changed

    • DatasetsManager.get and DatasetsManager.delete accept optional organizationId and projectId.
    • The tenancy is stashed on the returned Dataset handle and forwarded to every downstream storage call (getDetails, update, addItem, item batch ops, startExperimentAsync).
    • The abstract storage contract (getDatasetById, deleteDataset) gained an optional filters?: DatasetTenancyFilters arg.
    • Item-mutation inputs (AddDatasetItemInput, UpdateDatasetItemInput, BatchInsertItemsInput, BatchDeleteItemsInput) and UpdateDatasetInput accept optional filters for the internal existence check.

    Behavior

    • Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
    • On tenancy mismatch, get throws NOT_FOUND (returns null at the storage layer) and delete is 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, and datasetItemId fields 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-level batchId, datasetId, and datasetItemId when persisting a score for a stored trace.
    • ScoreRowData and score save payloads now include nullable batchId, datasetId, and datasetItemId.
    • 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 organizationId and projectId fields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and the listScoresBy* methods accept a filters option 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' },
    });

    projectId identifies the project scope, separate from resourceId which continues to mean the agent memory resource.

  • Raise @mastra/core peer floor to >=1.49.0-0 on all storage adapters so the tenancy-related named exports the adapters now consume are guaranteed to exist at install time. (#18861)

  • Scoped getDatasetById and deleteDataset to tenancy filters when the caller passes organizationId / projectId. (#18750)

    The adapters now push the tenancy predicate into the SQL/query when the new optional filters argument is present. Legacy calls that omit tenancy are unchanged. On mismatch, getDatasetById returns null and deleteDataset is 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 organizationId and projectId query parameters to the dataset routes. (#18750)

    GET /datasets/:datasetId, PATCH /datasets/:datasetId, and DELETE /datasets/:datasetId now accept optional tenancy query parameters. When provided, they are forwarded to mastra.datasets.get / .delete and 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 isError flag), and MCPClient did not check that flag.

    Now isError: true results 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 retention config, MongoDBStore can prune old documents from every growth domain it implements: memory (threads, messages, resources by createdAt), observability (spans by startedAt), scores (by createdAt), workflows (run snapshots by updatedAt), backgroundTasks (by completedAt, so in-flight tasks are never pruned), experiments (whole runs by completedAt, results cascade with their parent — transactional on replica sets), notifications (by createdAt), and schedules fire history (by actual_fire_at). (#18798)

    Deletes run in batches via bounded find(_id) + deleteMany pairs (bounded, resumable, and cancellable) so they stay safe on large collections. Anchor-field 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 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, and deleteDataset. (#18750)

    You can now pass organizationId and projectId to scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throw DATASET_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 accepts targetType, targetIds (overlap/union semantics), and name (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/core but on an older storage adapter, the new targetType/targetIds/name filter 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 applies search and pagination at the storage layer when version is provided alongside any of those. Previously they were silently dropped whenever version was set. The return shape is unchanged: passing only version still returns a bare DatasetItem[] snapshot; passing search, page, or perPage (with or without version) returns the paginated { items, pagination } shape. The bare-array branch is marked @deprecated; prefer passing page / perPage to always receive the paginated shape.

  • Tenancy-scope experiments getById and delete* on ExperimentsStorage. (#18770)

    ExperimentsStorage.getExperimentById, getExperimentResultById, deleteExperiment, and deleteExperimentResults used 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 optional filters: { organizationId?, projectId? } argument that is enforced on every adapter (inmemory, libsql, pg, mysql, mongodb, spanner):

    • On tenancy mismatch, get* returns null at the storage layer.
    • On tenancy mismatch, delete* is a silent no-op.
    • The tenancy predicate is folded into the destructive DML itself (scoped WHERE on 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.deleteDataset across 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.getExperiment and the shared experiment-ownership gate on Dataset now 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 filters are 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 },
    });
  • Fixed createExperiment in the MongoDB store persisting agentVersion as null regardless of the input. listExperiments already accepts an agentVersion filter, but rows created by this backend would never match it. New experiments now round-trip agentVersion end-to-end. (#18769)

  • Fixed a cross-tenant data-access issue on datasets by scoping DatasetsManager.get and DatasetsManager.delete to tenancy filters. (#18750)

    Previously get({ id }) and delete({ id }) looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of which organizationId / projectId it belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).

    What changed

    • DatasetsManager.get and DatasetsManager.delete accept optional organizationId and projectId.
    • The tenancy is stashed on the returned Dataset handle and forwarded to every downstream storage call (getDetails, update, addItem, item batch ops, startExperimentAsync).
    • The abstract storage contract (getDatasetById, deleteDataset) gained an optional filters?: DatasetTenancyFilters arg.
    • Item-mutation inputs (AddDatasetItemInput, UpdateDatasetItemInput, BatchInsertItemsInput, BatchDeleteItemsInput) and UpdateDatasetInput accept optional filters for the internal existence check.

    Behavior

    • Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
    • On tenancy mismatch, get throws NOT_FOUND (returns null at the storage layer) and delete is 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 organizationId and projectId fields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and the listScoresBy* methods accept a filters option 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' },
    });

    projectId identifies the project scope, separate from resourceId which continues to mean the agent memory resource.

  • Raise @mastra/core peer floor to >=1.49.0-0 on all storage adapters so the tenancy-related named exports the adapters now consume are guaranteed to exist at install time. (#18861)

  • Scoped getDatasetById and deleteDataset to tenancy filters when the caller passes organizationId / projectId. (#18750)

    The adapters now push the tenancy predicate into the SQL/query when the new optional filters argument is present. Legacy calls that omit tenancy are unchanged. On mismatch, getDatasetById returns null and deleteDataset is 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 organizationId and projectId query parameters to the dataset routes. (#18750)

    GET /datasets/:datasetId, PATCH /datasets/:datasetId, and DELETE /datasets/:datasetId now accept optional tenancy query parameters. When provided, they are forwarded to mastra.datasets.get / .delete and 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, and datasetItemId fields 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-level batchId, datasetId, and datasetItemId when persisting a score for a stored trace.
    • ScoreRowData and score save payloads now include nullable batchId, datasetId, and datasetItemId.
    • 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 organizationId and projectId fields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and the listScoresBy* methods accept a filters option 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' },
    });

    projectId identifies the project scope, separate from resourceId which continues to mean the agent memory resource.

@mastra/mysql@0.3.3

Patch Changes

  • Added optional tenancy arguments to getDataset, updateDataset, and deleteDataset. (#18750)

    You can now pass organizationId and projectId to scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throw DATASET_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 listExperiments in the MySQL store ignoring targetType, targetId, agentVersion, and status filters. Queries now correctly narrow on these fields, matching the behavior of the other stores (Postgres, LibSQL, Spanner, in-memory). (#18769)

    Also persisted agentVersion on experiment rows in the MySQL store. The column existed in the schema but createExperiment never wrote it and getExperimentById/listExperiments never returned it, so filtering by agentVersion would have matched nothing on rows created by this backend. New experiments now round-trip agentVersion end-to-end. Existing tables gain the column via the init() backfill.

  • Pushed remaining dataset read filters and pagination down to storage. (#18710)

    DatasetsManager.list({ filters }) now accepts targetType, targetIds (overlap/union semantics), and name (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/core but on an older storage adapter, the new targetType/targetIds/name filter 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 applies search and pagination at the storage layer when version is provided alongside any of those. Previously they were silently dropped whenever version was set. The return shape is unchanged: passing only version still returns a bare DatasetItem[] snapshot; passing search, page, or perPage (with or without version) returns the paginated { items, pagination } shape. The bare-array branch is marked @deprecated; prefer passing page / perPage to always receive the paginated shape.

  • Tenancy-scope experiments getById and delete* on ExperimentsStorage. (#18770)

    ExperimentsStorage.getExperimentById, getExperimentResultById, deleteExperiment, and deleteExperimentResults used 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 optional filters: { organizationId?, projectId? } argument that is enforced on every adapter (inmemory, libsql, pg, mysql, mongodb, spanner):

    • On tenancy mismatch, get* returns null at the storage layer.
    • On tenancy mismatch, delete* is a silent no-op.
    • The tenancy predicate is folded into the destructive DML itself (scoped WHERE on 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.deleteDataset across 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.getExperiment and the shared experiment-ownership gate on Dataset now 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 filters are 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 },
    });
  • Filled a pre-existing CRUD gap so the new dataset filter API works end-to-end on MySQL. (#18710)

    createDataset, updateDataset, and mapDataset now persist and hydrate targetType, targetIds, scorerIds, tags, and requestContextSchema. The columns were already declared by the shared schema but were never written or read, so listDatasets({ filters: { targetType, targetIds, name } }) would have matched nothing on MySQL before this fix. alterTable.ifNotExists was widened so in-place upgrades pick up the columns for older databases.

    Also fixed a mapItem row 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 previous parseJSON helper then tried to JSON.parse it again and silently returned undefined. It now falls back to the raw string when re-parsing fails, so versioned listItems({ search }) results round-trip the original input.

  • Fixed a cross-tenant data-access issue on datasets by scoping DatasetsManager.get and DatasetsManager.delete to tenancy filters. (#18750)

    Previously get({ id }) and delete({ id }) looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of which organizationId / projectId it belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).

    What changed

    • DatasetsManager.get and DatasetsManager.delete accept optional organizationId and projectId.
    • The tenancy is stashed on the returned Dataset handle and forwarded to every downstream storage call (getDetails, update, addItem, item batch ops, startExperimentAsync).
    • The abstract storage contract (getDatasetById, deleteDataset) gained an optional filters?: DatasetTenancyFilters arg.
    • Item-mutation inputs (AddDatasetItemInput, UpdateDatasetItemInput, BatchInsertItemsInput, BatchDeleteItemsInput) and UpdateDatasetInput accept optional filters for the internal existence check.

    Behavior

    • Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
    • On tenancy mismatch, get throws NOT_FOUND (returns null at the storage layer) and delete is 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, and datasetItemId fields 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-level batchId, datasetId, and datasetItemId when persisting a score for a stored trace.
    • ScoreRowData and score save payloads now include nullable batchId, datasetId, and datasetItemId.
    • 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 organizationId and projectId fields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and the listScoresBy* methods accept a filters option 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' },
    });

    projectId identifies the project scope, separate from resourceId which continues to mean the agent memory resource.

  • Raise @mastra/core peer floor to >=1.49.0-0 on all storage adapters so the tenancy-related named exports the adapters now consume are guaranteed to exist at install time. (#18861)

  • Scoped getDatasetById and deleteDataset to tenancy filters when the caller passes organizationId / projectId. (#18750)

    The adapters now push the tenancy predicate into the SQL/query when the new optional filters argument is present. Legacy calls that omit tenancy are unchanged. On mismatch, getDatasetById returns null and deleteDataset is 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 organizationId and projectId query parameters to the dataset routes. (#18750)

    GET /datasets/:datasetId, PATCH /datasets/:datasetId, and DELETE /datasets/:datasetId now accept optional tenancy query parameters. When provided, they are forwarded to mastra.datasets.get / .delete and 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 retention config, PostgresStore can prune old rows from every growth-table domain it implements: memory (threads, messages, resources by createdAtZ), observability (spans by startedAtZ), scores (by createdAtZ), workflows (run snapshots by updatedAtZ), backgroundTasks (by completedAtZ, so in-flight tasks are never pruned), experiments (whole runs by completedAtZ, results cascade with their parent), notifications (by createdAtZ), and schedules fire history (by actual_fire_at). (#18733)

    Deletes run in batches via ctid subqueries (bounded, resumable, and cancellable) 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; 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 via drop_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, and deleteDataset. (#18750)

    You can now pass organizationId and projectId to scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throw DATASET_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 accepts targetType, targetIds (overlap/union semantics), and name (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/core but on an older storage adapter, the new targetType/targetIds/name filter 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 applies search and pagination at the storage layer when version is provided alongside any of those. Previously they were silently dropped whenever version was set. The return shape is unchanged: passing only version still returns a bare DatasetItem[] snapshot; passing search, page, or perPage (with or without version) returns the paginated { items, pagination } shape. The bare-array branch is marked @deprecated; prefer passing page / perPage to always receive the paginated shape.

  • Tenancy-scope experiments getById and delete* on ExperimentsStorage. (#18770)

    ExperimentsStorage.getExperimentById, getExperimentResultById, deleteExperiment, and deleteExperimentResults used 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 optional filters: { organizationId?, projectId? } argument that is enforced on every adapter (inmemory, libsql, pg, mysql, mongodb, spanner):

    • On tenancy mismatch, get* returns null at the storage layer.
    • On tenancy mismatch, delete* is a silent no-op.
    • The tenancy predicate is folded into the destructive DML itself (scoped WHERE on 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.deleteDataset across 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.getExperiment and the shared experiment-ownership gate on Dataset now 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 filters are 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 },
    });
  • Fixed a double-encoding bug where createDataset and updateDataset stored targetIds and scorerIds as JSON-encoded strings into the JSONB columns instead of arrays. This caused the new listDatasets({ 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 targetIds filter. They self-heal on the next updateDataset call. Deployments with a long tail of pre-existing datasets should run a one-time backfill (re-encoding targetIds and scorerIds on 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.get and DatasetsManager.delete to tenancy filters. (#18750)

    Previously get({ id }) and delete({ id }) looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of which organizationId / projectId it belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).

    What changed

    • DatasetsManager.get and DatasetsManager.delete accept optional organizationId and projectId.
    • The tenancy is stashed on the returned Dataset handle and forwarded to every downstream storage call (getDetails, update, addItem, item batch ops, startExperimentAsync).
    • The abstract storage contract (getDatasetById, deleteDataset) gained an optional filters?: DatasetTenancyFilters arg.
    • Item-mutation inputs (AddDatasetItemInput, UpdateDatasetItemInput, BatchInsertItemsInput, BatchDeleteItemsInput) and UpdateDatasetInput accept optional filters for the internal existence check.

    Behavior

    • Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
    • On tenancy mismatch, get throws NOT_FOUND (returns null at the storage layer) and delete is 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, and datasetItemId fields 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-level batchId, datasetId, and datasetItemId when persisting a score for a stored trace.
    • ScoreRowData and score save payloads now include nullable batchId, datasetId, and datasetItemId.
    • 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 organizationId and projectId on the definition record, and list/listResolved accept 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 organizationId and projectId fields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and the listScoresBy* methods accept a filters option 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' },
    });

    projectId identifies the project scope, separate from resourceId which continues to mean the agent memory resource.

  • Raise @mastra/core peer floor to >=1.49.0-0 on all storage adapters so the tenancy-related named exports the adapters now consume are guaranteed to exist at install time. (#18861)

  • Scoped getDatasetById and deleteDataset to tenancy filters when the caller passes organizationId / projectId. (#18750)

    The adapters now push the tenancy predicate into the SQL/query when the new optional filters argument is present. Legacy calls that omit tenancy are unchanged. On mismatch, getDatasetById returns null and deleteDataset is 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 organizationId and projectId query parameters to the dataset routes. (#18750)

    GET /datasets/:datasetId, PATCH /datasets/:datasetId, and DELETE /datasets/:datasetId now accept optional tenancy query parameters. When provided, they are forwarded to mastra.datasets.get / .delete and 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-ui root 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';

    mastracode now uses the exact subpath imports, and lint rules prevent new broad @mastra/playground-ui imports.

  • Removed the Searchbar component from @mastra/playground-ui. Compose search inputs with InputGroup instead 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, and MASTRA_PLATFORM_OBSERVABILITY_ENDPOINT to the browser so the Signals page can call the Entity-Learning API. The route is gated on the platform observability config, and the MASTRA_SIGNALS_UI flag 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 via credentials: 'include', and scopes reads with the X-Mastra-Project-Id header — 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. Pass checkpointName to 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 organizationId and projectId fields to scores for multi-tenant isolation. Scores can now be saved with tenancy metadata and the listScoresBy* methods accept a filters option 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' },
    });

    projectId identifies the project scope, separate from resourceId which 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 organizationId and projectId query parameters to the dataset routes. (#18750)

    GET /datasets/:datasetId, PATCH /datasets/:datasetId, and DELETE /datasets/:datasetId now accept optional tenancy query parameters. When provided, they are forwarded to mastra.datasets.get / .delete and 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, and deleteDataset. (#18750)

    You can now pass organizationId and projectId to scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throw DATASET_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 to server.auth or studio.auth, failing with Property '#private' is missing (#18682). (#18796)

    Auth providers are now typed with a new structural IMastraAuthProvider interface (exported from @mastra/core/server and @mastra/auth), so provider packages no longer need a shared class identity with @mastra/core. CompositeAuth also accepts any IMastraAuthProvider implementation. 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.get and DatasetsManager.delete to tenancy filters. (#18750)

    Previously get({ id }) and delete({ id }) looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of which organizationId / projectId it belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).

    What changed

    • DatasetsManager.get and DatasetsManager.delete accept optional organizationId and projectId.
    • The tenancy is stashed on the returned Dataset handle and forwarded to every downstream storage call (getDetails, update, addItem, item batch ops, startExperimentAsync).
    • The abstract storage contract (getDatasetById, deleteDataset) gained an optional filters?: DatasetTenancyFilters arg.
    • Item-mutation inputs (AddDatasetItemInput, UpdateDatasetItemInput, BatchInsertItemsInput, BatchDeleteItemsInput) and UpdateDatasetInput accept optional filters for the internal existence check.

    Behavior

    • Omitting tenancy preserves the existing behavior (no predicate added) — fully backwards compatible.
    • On tenancy mismatch, get throws NOT_FOUND (returns null at the storage layer) and delete is 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 getDatasetById and deleteDataset to tenancy filters when the caller passes organizationId / projectId. (#18750)

    The adapters now push the tenancy predicate into the SQL/query when the new optional filters argument is present. Legacy calls that omit tenancy are unchanged. On mismatch, getDatasetById returns null and deleteDataset is 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, and deleteDataset. (#18750)

    You can now pass organizationId and projectId to scope dataset reads, updates, and deletes to a specific tenant. Reads and updates against a dataset in a different tenant throw DATASET_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 accepts targetType, targetIds (overlap/union semantics), and name (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/core but on an older storage adapter, the new targetType/targetIds/name filter 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 applies search and pagination at the storage layer when version is provided alongside any of those. Previously they were silently dropped whenever version was set. The return shape is unchanged: passing only version still returns a bare DatasetItem[] snapshot; passing search, page, or perPage (with or without version) returns the paginated { items, pagination } shape. The bare-array branch is marked @deprecated; prefer passing page / perPage to always receive the paginated shape.

  • Tenancy-scope experiments getById and delete* on ExperimentsStorage. (#18770)

    ExperimentsStorage.getExperimentById, getExperimentResultById, deleteExperiment, and deleteExperimentResults used 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 optional filters: { organizationId?, projectId? } argument that is enforced on every adapter (inmemory, libsql, pg, mysql, mongodb, spanner):

    • On tenancy mismatch, get* returns null at the storage layer.
    • On tenancy mismatch, delete* is a silent no-op.
    • The tenancy predicate is folded into the destructive DML itself (scoped WHERE on 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.deleteDataset across 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.getExperiment and the shared experiment-ownership gate on Dataset now 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 filters are 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 },
    });
  • Fixed a cross-tenant data-access issue on datasets by scoping DatasetsManager.get and DatasetsManager.delete to tenancy filters. (#18750)

    Previously get({ id }) and delete({ id }) looked up a dataset by its primary key alone. Any caller who knew a dataset id could read or delete it regardless of which organizationId / projectId it belonged to. This is now closed at the storage layer via a scoped SQL predicate (option (a) — no fetch-then-assert).

    What changed

    • DatasetsManager.get and DatasetsManager.delete accept optional organizationId and projectId.
    • The tenancy is stashed on the returned Dataset handle and forwarded to every downstream storage call (getDetails, update, addItem, item batch ops, startExperimentAsync).
    • The abstract storage contract (getDatasetById, deleteDataset) gained an optional filters?: DatasetTenancyFilters arg.
    • Item-mutation inputs (AddDatasetItemInput, UpdateDatasetItemInput, BatchInsertItemsInput, BatchDeleteItemsInput) and UpdateDatasetInput accept optional filters for the internal existence check.

    Behavior

    • Omitting