Skip to content

November 2, 2026

Latest

Choose a tag to compare

@PaulieScanlon PaulieScanlon released this 02 Nov 12:33
· 68 commits to main since this release

Highlights

MCP Tasks (Storage-backed, restart-safe long-running execution)

@mastra/mcp adds experimental Mastra-storage-backed MCP tasks so long-running tools/agents/workflows can return immediately with a task, be polled/answered/cancelled across instances, and survive restarts with shared storage and unified tracing.

Background Tasks for Non-Agent Owners (Retention, Concurrency, Queued Resume)

Mastra background tasks now support non-agent owners (e.g., MCP servers) with per-task expiresAt/metadata retention, per-owner concurrency overrides, and { onNoSlot: 'queue' } to queue resumes until capacity frees up (with storage adapters updated to persist the new fields).

Observability: Outcomes/Stop Reasons + Model/Provider + TTFT Across Queries and Stores

Observability now records execution outcome + typed stopReason (and context), exports model/provider, and measures timeToFirstTokenMs (TTFT); these fields are queryable/groupable in clients/servers and supported by major stores (ClickHouse/DuckDB/Postgres), enabling better failure analysis and model/provider cost/perf breakdowns.

Dashboards and Saved Views as Schemas + UI Rendering/Sharing

Core adds browser-safe schemas for custom dashboard definitions and saved observability list views, while @mastra/playground-ui introduces DashboardRenderer plus first-class Saved Views for Traces/Spans/Threads/Logs (URL↔view helpers, view picker, and host-provided persistence).

Agents + Workspaces: Authenticated Channel Attachments & Workspace File UX

Channel agents can now download authenticated binary attachments (e.g., Slack ZIP/DOCX) into channel-attachments/ with storage caps/policies, and text attachments (CSV/JSON/YAML/logs) are fetched and inlined as text; workspace write protection is now opt-in (via requireReadBeforeWrite) to reduce false StaleFileError failures.

Breaking Changes

  • @mastra/playground-ui: Metrics chart card API cleanup (e.g., removed ChartCard description/summary, removed several MetricsCard.* components, changed related table/legend APIs).
  • @mastra/playground-ui: Removed fluid-hover utilities (useFluidHover, FluidHoverHighlight) and CommandList.highlightClassName.
  • @mastra/core: Removed runtime-importable subpaths @mastra/core/network/vNext and @mastra/core/telemetry/otel-vendor (they pointed to files that no longer ship).
  • @mastra/factory: defineBoard() and MastraFactory boards/includeDefaultBoards are deprecated in favor of stored boards APIs; code reading deprecated board fields (title/role/next) should move to name/runner.context/nextPhases.

Changelog

@mastra/core@1.81.0

Minor Changes

  • Added schemas for saved observability list views. Use savedViewStateSchema to validate a saved traces, spans, threads or logs view: its filters, time range, sort, columns and page options. A service that only stores views can use savedViewStateEnvelopeSchema instead. It checks the version, the page and the 16 KiB size limit. Dashboards can reuse the filter shape through filterNodeSchema. It accepts any operator id, so new operators don't need a schema change. (#28715)

    import { savedViewStateSchema } from '@mastra/core/storage';
    
    savedViewStateSchema.parse({
      schemaVersion: 1,
      page: 'traces',
      filters: [{ fieldId: 'status', value: 'error' }],
      dateRange: { preset: 'last-24h' },
      sort: { field: 'startedAt', direction: 'desc' },
    });
  • Channel agents with a workspace can now open binary file attachments, such as ZIP and DOCX files, that need an authenticated download (for example Slack files). The file is downloaded through the adapter and saved under channel-attachments/ in the agent's workspace. The agent gets the workspace path instead of a private URL it can't open. (#28913)

    This covers attachments that inlineMedia doesn't send to the model as file parts. PDFs are sent inline by default.

    When the workspace uses mounts, the file is saved in the first writable mount other than /. Saving follows the workspace's write-file tool policy: if mastra_workspace_write_file is disabled or needs approval, the file isn't saved. Adapters whose links don't need a token also get the workspace path. For example, a Discord agent with a writable workspace now gets the workspace path instead of the CDN URL, which expires.

    The agent still gets the URL for files over 10 MiB, failed downloads, read-only workspaces, workspaces where writes are disabled or need approval, and agents without a workspace.

    channel-attachments/ is capped at 100 MiB per workspace so channel participants can't fill the disk. Once a file would go over the cap, the agent gets the URL instead and nothing is written. Change the cap with maxAttachmentStorageBytes:

    new Agent({
      // ...
      channels: { adapters: { slack }, maxAttachmentStorageBytes: 500 * 1024 * 1024 },
    });
  • Added setCookies() to MastraBrowser, so your app can sign a thread's browser in to a site the agent can't sign in to itself, such as a preview behind your own login. The cookie value never passes through the model. Cookies set before the browser opens are added when it does. (#28631)

    await browser.setCookies([{ name: 'my_app_session', value: token, domain: 'app.example.com', path: '/' }], threadId);

    Browser providers that don't support cookies throw. Custom providers can add support by overriding addCookiesToOpenBrowser() and calling addPendingCookies(threadId) once a thread's browser opens, before it loads a page.

  • Added factory-scoped sandbox environment. A deployment that hosts several factories can now give a credential to one factory's sessions only: every Factory sandbox provider accepts env as a function of the session's sandbox context, which carries the new factoryId and factoryName fields. Scope secrets by factoryId, because names can be edited and aren't unique. (#28767)

    new PlatformFactorySandbox({
      env: ctx => (ctx.factoryId === PLATFORM_FACTORY_ID ? { PLATFORM_TOKEN } : {}),
    });

    Platform and Docker sandboxes keep the environment they started with when they resume. When you remove a credential from a factory, recreate its running sandboxes.

    Also in: @mastra/docker@0.13.0, @mastra/e2b@0.16.0, @mastra/factory@0.26.0, @mastra/platform-workspace@1.11.0

  • Added execution outcomes and typed stop-reason details to agent, model, tool, and workflow traces. Traces distinguish failures, cancellations, limits, guardrail blocks, and observer interruptions; TripWire errors now use the name TripWire instead of Error, including in string representations, stacks, and serialized errors, so they can be identified across workers. (#28703)

    Custom spans can record an outcome with a compact stop-reason context:

    import { SpanType, type ObservabilityInstance } from '@mastra/core/observability';
    
    function recordGuardrailBlock(observability: ObservabilityInstance, processorId: string) {
      const span = observability.startSpan({ name: 'policy check', type: SpanType.GENERIC });
      span.end({ outcome: 'blocked', stopReason: 'guardrail', stopReasonContext: { processorId } });
    }

    The public outcome, stopReason, and stopReasonContext fields now also allow null. Exporter and other consumer types must handle null alongside an absent value; arbitrary custom outcome strings and context objects remain supported. Workflow events and durable snapshots can now include tripwire details, including processor-supplied metadata.

    Agent and generation spans also close when the internal workflow fails without a finish callback, while errors followed by a finish retain their final usage and output.

  • Added an optional inputAccepted field to OutputResult, the result argument of processOutputResult. It is set only when finishReason is 'error', and is true when the run accepted the turn's input before it failed. MessageHistory uses it to keep the user message of a failed turn that produced no output, so an accepted turn is not lost from history. Failed turns without the flag are still dropped, as before. (#28988)

    import type { Processor } from '@mastra/core/processors';
    
    const processor: Processor = {
      id: 'accepted-turns',
      async processOutputResult({ messages, result }) {
        if (result.finishReason === 'error' && result.inputAccepted) {
          // The run failed after accepting this turn's input
        }
        return messages;
      },
    };
  • Added an optional getRoutes() method to SignalProvider. Routes it returns are added to server.apiRoutes automatically when the agent is registered, so webhook and OAuth routes for a signal provider no longer need manual server config. (#28575)

    class AcmeSignals extends SignalProvider<'acme'> {
      readonly id = 'acme' as const;
    
      getRoutes() {
        return [registerApiRoute('/acme/webhook', { method: 'POST', handler: async c => c.json({ ok: true }) })];
      }
    }
    
    // Before: server: { apiRoutes: acme.getRoutes() }
    // After: just add the provider to the agent
    const agent = new Agent({ /* ... */ signals: [new AcmeSignals()] });
    export const mastra = new Mastra({ agents: { agent } });
  • Fixed resumed runs losing the link to the run they continue. When you resume an agent, durable agent or workflow run, its span now sets resumedFromSpanId to the span id of the first run. This stays true after several resumes and after a durable agent recovers. A resumed run also keeps the external parent span of the run it continues. (#28849)

    A resume that passes only tracingOptions.parentSpanId now stays in the original trace under the first run, and uses that id as its external parent. Before, it started a new trace.

    await agent.resumeGenerate({ approved: true }, { runId, toolCallId, tracingOptions: { parentSpanId: callerSpanId } });
    // The resumed run stays in the first run's trace.
    // resumedFromSpanId is the first run's span id, externalParentSpanId is callerSpanId.

    The resumedFromSpanId option of getOrCreateSpan is deprecated. Use resumeFrom with the saved tracing context instead.

  • Span aggregates can now group by model and provider and count their distinct values with countDistinct.model and countDistinct.provider. Stores opt in with the new span-aggregate-model-dimensions feature. (#28908)

    const { rows } = await mastraClient.aggregateSpans({
      timeRange,
      where: { op: 'eq', left: { path: 'spanType' }, right: { literal: 'model_generation' } },
      groupBy: ['provider', 'model'],
      measures: ['count', 'tokens.total.sum', 'cost.sum'],
    });
  • Span queries and span aggregates now accept model and provider filters. Stores opt in with the new span-query-model-fields feature. (#28859)

    const { spans } = await mastraClient.querySpans({
      timeRange,
      where: { op: 'eq', left: { path: 'provider' }, right: { literal: 'anthropic' } },
    });
  • Fixed the browser failing with ECONNREFUSED when launched again on a profile it had used before. (#28675)

    Before launching Chrome on a local profile, Mastra now clears the launcher logs left by earlier launches (chrome-err.log, chrome-out.log), which made the next launch connect to the old, dead DevTools port. It also removes a Chrome profile lock left by a browser that is no longer running. Stagehand does this for every thread-scoped browser too, not only on the first launch. Remote browsers (cdpUrl, Browserbase) are not affected.

    If a browser that is still running holds the profile, launch now fails with a BrowserProfileInUseError that names the profile and the holding process:

    import { BrowserProfileInUseError } from '@mastra/core/browser';
    
    try {
      await browser.launch();
    } catch (error) {
      if (error instanceof BrowserProfileInUseError) {
        // Another Chrome is using the profile: close it, or use a different profile.
        console.error(error.message);
      }
      throw error;
    }

    Added browser.retire() and browser.killProcesses() for code that replaces one browser with another. close() lets the browser relaunch the next time something uses it; retire() closes it for good, so a tool call still holding the old browser gets an error instead of starting an untracked Chrome. killProcesses() force-kills the browser's processes when close() fails or hangs. It works on macOS and Linux; it cannot force-kill Chrome on Windows.

    // Replace oldBrowser with newBrowser without leaving its Chrome running.
    const retired = oldBrowser.retire();
    const timedOut = await Promise.race([
      retired.then(() => false),
      new Promise<boolean>(resolve => setTimeout(() => resolve(true), 5000)),
    ]).catch(() => true);
    if (timedOut) oldBrowser.killProcesses();

    A browser that finishes closing after another browser took its profile no longer deletes the new browser's lock.

    A lock whose holder is a live local process that has not started listening yet (Chrome creates the lock first) is no longer removed as stale; Mastra waits up to 3 seconds for it.

    Also in: @mastra/stagehand@0.4.0

  • Added schemas for custom dashboard definitions. These are schemas only: @mastra/core adds no storage methods for dashboards, and no store persists them. (#28714)

    Layout

    • A definition lays out rows of widgets on a 12-column grid.
    • Each widget stores its own trace aggregate, span aggregate, metrics, or scores queries. Queries have no time range; the page supplies it.
    • A view can set its own description, scope, and drilldown, overriding the widget's. A drilldown has an action target and a click target.

    Queries

    • A query can have an optional id. It starts with a letter, uses only letters, digits, _ and -, and is unique within its view. Column ids are also unique within a view.
    • Set compare: 'previousWindow' on any query to compare with the previous window.
    • A scores query without scorerId covers every scorer, with one series per scorer.

    Filters

    • defaults.filters and drilldown filters use the same filter node schema as saved views (filterNodeSchema).
    • Filter groups can nest at most 12 levels deep (DASHBOARD_MAX_FILTER_DEPTH).
    • The page's filters are combined with each query's own. where clauses are ANDed. If both set the same metrics or scores filter field, the values must agree (arrays intersect). A contradiction returns no data.

    Validation

    • dashboardDefinitionSchema validates a whole definition.
    • dashboardDefinitionEnvelopeSchema checks only what a service storing dashboards needs: a known schemaVersion, at most 20 widgets, 1 to 8 queries per view, at most 64 KiB serialized, unique widget ids, and kind and views on every widget. New widget kinds can be stored without upgrading that service.

    New widget kinds

    • Widget kind accepts any non-empty string. View options are passed through for each kind to validate, so new widget kinds need no @mastra/core upgrade.
    • DASHBOARD_WIDGET_KINDS lists the built-in kinds: kpis, table, timeseries and scores.

    Versions

    • Adding an optional field keeps schemaVersion the same.
    • A breaking change bumps schemaVersion and adds an upgrade step to migrateDashboardDefinition. That function brings any stored version up to the current one before parsing.

    Browser apps

    • Import the dashboard schemas from @mastra/core/storage/dashboards. It loads no Node built-ins and uses no Node globals such as process or Buffer.
    • @mastra/core/storage reaches agent code that reads process when it loads, so it fails in the browser.
    import { dashboardDefinitionSchema } from '@mastra/core/storage/dashboards';
    
    const result = dashboardDefinitionSchema.safeParse({
      schemaVersion: 1,
      rows: [
        {
          widgets: [
            {
              id: 'agents',
              span: 6,
              kind: 'table',
              views: [
                {
                  queries: [
                    {
                      source: 'traceAggregate',
                      measures: ['count', 'errorRate'],
                      groupBy: ['rootEntityName'],
                    },
                  ],
                },
              ],
            },
          ],
        },
      ],
    });
  • Added outcome and stopReason to advanced span and trace queries. You can now filter, group, and discover values by how a span ended and why, separately from status. Span and trace results also return stopReasonContext. (#28884)

    At trace scope the fields read the root span. Use spans.some to find traces where any span hit a limit:

    const { traces } = await client.queryTraces({
      timeRange,
      where: { spans: { some: { op: 'eq', left: { path: 'outcome' }, right: { literal: 'limit' } } } },
    });

    A missing outcome means "not recorded", never success. Stores opt in through the new span-query-outcome-fields and trace-query-outcome-fields features.

  • Trace, span and thread queries can now return and filter by timeToFirstTokenMs, the time from the start of the model call to its first streamed chunk. Trace and span queries can also sort by it. Trace rows use the root span's value, and thread rows use the first trace's root agent. Missing values sort last. Stores that support it declare the trace-query-time-to-first-token and trace-query-time-to-first-token-sort features. (#28699)

    const page = await storage.queryTraces(
      planTraceQuery(
        parseTraceQueryRequest({ timeRange, orderBy: [{ field: 'timeToFirstTokenMs', direction: 'desc' }] }),
      ),
    );

    Durable agents now carry the time to first token of the model call that streamed the run's first chunk across steps, so their generation and agent run spans report it too.

    A model call that fails before a fallback model, or a durable agent's retry, takes over now ends its model inference span with that error.

  • Added support for background tasks owned by code other than agents, such as MCP servers. (#28425)

    Per-task retention. Tasks accept optional expiresAt and metadata fields. When the cleanup option is set, cleanup deletes a finished task once its own expiresAt passes instead of using completedTtlMs or failedTtlMs. Pending, running, and suspended tasks are never deleted. metadata is stored with the task and not read by the manager.

    await mastra.backgroundTaskManager.enqueue({
      agentId: 'mcp-server:ops',
      toolName: 'mcp-server:ops:deploy',
      toolCallId,
      runId,
      args,
      expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
      metadata: { ttlMs: 86_400_000 },
    });

    Per-owner concurrency. perOwnerConcurrency overrides perAgentConcurrency for specific owners, keyed by agentId. Each value must be a number of at least 1. These tasks still count toward globalConcurrency.

    new Mastra({
      backgroundTasks: { enabled: true, globalConcurrency: 60, perOwnerConcurrency: { 'mcp-server:ops': 50 } },
    });

    Queued resume. resume() still throws when no concurrency slot is free. Pass { onNoSlot: 'queue' } to keep the task suspended and resume it once a slot frees up. The queue is held in memory by the calling process, so a queued resume is lost if that process exits; the task stays suspended and can be resumed again. A queued resume is dropped if the task is cancelled or resumed elsewhere first.

    await mastra.backgroundTaskManager.resume(taskId, resumeData, { onNoSlot: 'queue' });

    Storage adapters need a matching release to store expiresAt and metadata. With an older adapter, cleanup detects that expiry is unsupported and keeps using only the global retention settings, as before.

  • Added the experimental getTasksInfo() and getTasksInspector() methods to MCP servers, so tools like Studio can see which tools run as MCP tasks and list, cancel or answer a server's tasks. (#28629)

    const inspector = server.getTasksInspector();
    const { tasks } = (await inspector?.list({ status: 'input_required' })) ?? { tasks: [] };
    if (tasks[0]) await inspector?.cancel(tasks[0].taskKey);

Patch Changes

  • Fixed resuming suspended agent runs across server instances when a process retains an earlier run segment. (#29061)

  • Update provider registry and model documentation with latest models and providers (9f03685)

  • Fixed recovered durable agents skipping configured completion checks (#29037)

  • Fixed background tasks staying locked to a worker after it shut down gracefully. (#28719)

  • Fixed provider-reported cost on model generation spans when an agent run pauses on a tool approval or suspend() and then resumes. (#28955)

  • Fixed @mastra/core/loop/server failing to load with a module-not-found error. (#29056)

  • PROCESSOR_WORKFLOW_FAILED now keeps the underlying error as its cause. (#28831)

  • Fixed POST /agents/:agentId/runs/:runId/abort answering { aborted: false } when it reached a server that does not own the run. (#28522)

    Also in: @mastra/inngest@1.11.0, @mastra/server@1.81.0

  • Flush observability exporters before closing storage on Mastra shutdown, so final spans persist when using DuckDB. (#28656)

    Also in: @mastra/code-sdk@1.17.0, @mastra/duckdb@1.17.0

  • Fixed output.request and onFinish.request for durable and evented agents. (#28674)

  • Fixed agents treating observationalMemory: { enabled: false } as if observational memory were on. (#28922)

  • Fixed resumeStream() on evented workflows rejecting its result with "promise 'result' was not resolved or rejected when stream finished". (#28871)

  • Added an expectedSuspendedAt condition to background task updates. (#28590)

  • Added a pinToWorker option to backgroundTaskManager.enqueue() for tasks whose work is already running in the current process. (#28507)

  • Fixed nested evented workflows running without the caller's workflows:execute permission on the child workflow. (#28669)

  • Added the internal support @mastra/memory needs to delete a thread's event stream when the thread is deleted. (#28968)

  • Fixed durable runs stopping when a lease renewal queued behind other storage writes still belongs to the same execution. (#28936)

  • Fixed trace trees for agents whose model call is rejected and handled by an error processor (for example a retry on a rate limit). (#28745)

  • Inngest durable agents no longer block memory writes after modelSettings.timeout.totalMs expires. (#28926)

    Also in: @mastra/inngest@1.11.0

  • Fixed thread-message listeners accumulating while durable runs wait for approval. (#29046)

  • Fixed tracing for runs that continue in another process. (#28832)

  • Fixed supervisor agents losing track of which subagent tool paused. (#28991)

  • Improved Mastra deployer compatibility: the deployer option now accepts any object that implements the IDeployer interface (exported from @mastra/core/deployer), not only subclasses of MastraDeployer. (#28428)

  • Retry policies that match bad-request errors no longer retry a 400 that rejects the model or a parameter, such as model_not_found. (#28786)

  • Fixed thread stream topics using far more memory than needed for agents without memory, such as the observational memory observer and reflector. (#28746)

  • Fixed channel tool approval actions on tool calls whose ID contains a colon. (#28728)

  • Inngest durable agents now save the accepted user message before the run starts, as the other durable engines do. (#28747)

    Also in: @mastra/inngest@1.11.0

  • Clarify that FileStat.path is a path within the filesystem namespace. (#28819)

  • Fixed subscribeToThread({ withInitialHistory: true }) hanging or rejecting when the lease backend (for example Redis) is slow or unavailable, which also kept AgentController sessions from opening. (#28924)

  • Fixed provider-executed tool results that stream partial values, such as OpenAI image_generation partial images with AI SDK v6. (#28903)

    Also in: @mastra/ai-sdk@1.11.3

  • Fixed signals and notifications that arrive while a run is finishing. (#29015)

  • Fixed AgentController sessions getting stuck as "running" after a failed tool answer. (#29005)

  • Fixed workspace sandbox tools (execute_command, get_process_output, kill_process) sending an empty tool result to the model when a beforeToolCall hook blocked the call with { error: reason }. (#28919)

  • Fixed channel agents being unable to read text attachments such as .txt, CSV, JSON, YAML or log files shared in Slack. (#28901)

  • Studio's model provider picker now lists Mastra Gateway even when MASTRA_GATEWAY_API_KEY isn't set. (#28824)

  • Publish tool suspension and approval events only after the parked tool call is saved to memory, so a client that reacts to the event — resuming it, or aborting the run — finds the tool call already persisted instead of racing the save still in flight. (#28839)

  • Limit memory used when publishing stalls. (#28652)

  • Added the saved view schemas (savedViewStateSchema, savedViewStateEnvelopeSchema, SAVED_VIEW_STATE_MAX_BYTES and related types) to the browser-safe @mastra/core/storage/dashboards entry, so browser apps can validate a saved view before sending it. (#29001)

  • Fixed agent runs that hit modelSettings.timeout.totalMs losing the user's message from memory. (#28668)

  • Updated the AI SDK v5 compatibility dependencies so they resolve @ai-sdk/provider-utils 3.0.42, which blocks .internal hostnames in download URL validation and encodes provider-returned identifiers in request paths. (#28716)

    Also in: @mastra/agent-builder@1.1.32, @mastra/longmemeval@1.1.40

  • Fixed LocalSandbox.executeCommand(command, args) on native Windows. (#28900)

  • Workspace tools now save their data-workspace-metadata chunks to the current response message. (#28853)

  • Workspace file-modification protection is now off by default and only applies when requireReadBeforeWrite is enabled. (#28935)

  • write_file now names the real file location when the filesystem maps an absolute path somewhere else. (#28902)

@mastra/agent-browser@0.6.0

Minor Changes

  • Added setCookies() support to AgentBrowser. Cookies set before a thread's browser opens are added before it loads any page, so pages open signed in. (#28631)

    await browser.setCookies([{ name: 'my_app_session', value: token, domain: 'app.example.com', path: '/' }], threadId);
  • browser_screenshot can now also save the screenshot as a PNG in the agent's workspace. Pass a path and the agent can attach or share the file afterwards without setting up its own browser. Missing folders are created. Saving follows the workspace's write-file settings, so nothing is saved when writing is turned off or needs approval. If the screenshot can't be saved, for example because the agent has no workspace, the model still gets the image along with the reason. (#28636)

    { "path": "screenshots/home.png" }

@mastra/agent-builder@1.1.32

Patch Changes

  • Fixed require('@mastra/agent-builder') failing with a module-not-found error. (#29056)

  • Improved type declaration formatting. (#28726)

    Also in: @mastra/code-sdk@1.17.0, @mastra/deployer@1.81.0, @mastra/factory@0.26.0, @mastra/memory@1.40.0

@mastra/ai-sdk@1.11.3

Patch Changes

  • Added delegatedToolCallId and delegatedToolName to data-tool-call-suspended and data-tool-call-approval parts. (#28991)

@mastra/braintrust@1.3.25

Patch Changes

  • Fixed Braintrust cost for model calls with a provider-reported price, such as OpenRouter and Vercel AI Gateway. (#28954)

  • Spans keep timeToFirstTokenMs out of Braintrust metadata, as with completionStartTime; Braintrust already derives time to first token itself. (#28699)

@mastra/browser-firecrawl@0.3.0

Minor Changes

  • Added setCookies() support to FirecrawlBrowser. Cookies set before a thread's browser opens are added before it loads any page, so pages open signed in. (#28631)

    await browser.setCookies([{ name: 'my_app_session', value: token, domain: 'app.example.com', path: '/' }], threadId);

@mastra/browser-viewer@0.3.0

Minor Changes

  • Added setCookies() support to BrowserViewer. Cookies set before a thread's browser opens are added before it loads any page, so pages open signed in. (#28631)

    await browser.setCookies([{ name: 'my_app_session', value: token, domain: 'app.example.com', path: '/' }], threadId);

@mastra/clickhouse@1.28.0

Minor Changes

  • Added support for filtering and grouping span and trace queries by outcome and stopReason, and returned outcome, stopReason, and stopReasonContext on span and trace results. Trace-level values come from the root span. (#28884)

    With one of these stores configured, query traces whose root span hit a limit:

    const { traces } = await client.queryTraces({
      timeRange,
      where: { op: 'eq', left: { path: 'outcome' }, right: { literal: 'limit' } },
    });

    Also in: @mastra/duckdb@1.17.0, @mastra/pg@1.34.0

  • Span aggregates can group by model and provider, and count distinct models and providers. Like the model and provider filters, grouping reads only the dedicated model and provider columns, not span attributes. Spans exported before @mastra/observability filled the columns group under null; existing spans aren't backfilled. (#28908)

    const { rows } = await mastraClient.aggregateSpans({
      timeRange,
      where: { op: 'eq', left: { path: 'spanType' }, right: { literal: 'model_generation' } },
      groupBy: ['model'],
      measures: ['count', 'tokens.total.sum'],
    });
    // [{ dimensions: { model: 'claude-sonnet-5-5' }, measures: { count: 12, 'tokens.total.sum': 48210 } }, ...]

    Also in: @mastra/duckdb@1.17.0, @mastra/pg@1.34.0

  • Span queries and span aggregates can filter by model and provider. These filters read only the dedicated model and provider columns, not span attributes. @mastra/observability fills the columns only when a span has both a model and a provider, and versions before it filled them left them empty, so those spans count as missing: eq, in and exists don't match them, while ne, notIn and notExists do. Results still return model and provider for them, read from their attributes. (#28859)

    const { spans } = await mastraClient.querySpans({
      timeRange,
      where: { op: 'eq', left: { path: 'provider' }, right: { literal: 'openai' } },
    });

    Also in: @mastra/duckdb@1.17.0, @mastra/pg@1.34.0

  • ClickHouse trace, span and thread queries now return and filter by timeToFirstTokenMs, and trace and span queries can sort by it, slowest or fastest first. Traces use the root span's value. Traces and spans without a value come last. (#28699)

    orderBy: [{ field: 'timeToFirstTokenMs', direction: 'desc' }];

Patch Changes

  • Fixed span timestamps being stored as 9999-12-31 on ClickHouse 26.8 and later. (#28011)

  • Trace query spans.some and spans.none filters on model and provider now also match the dedicated span columns. (#28859)

    Also in: @mastra/duckdb@1.17.0, @mastra/pg@1.34.0

@mastra/client-js@1.58.0

Minor Changes

  • Workspace routes now report capabilities.hasDynamicFilesystem when the filesystem comes from a resolver. File routes can't browse that filesystem, so Studio's workspace page now explains that the filesystem is resolved per request instead of showing a file tree that fails to load, and the skill dialog warns admins that skill files can't be written to that workspace. (#29027)

    const info = await client.getWorkspace('my-workspace').info();
    
    if (info.capabilities?.hasDynamicFilesystem) {
      // The filesystem is created per request by a resolver, so the /fs routes can't browse it
    }

    Also in: @mastra/react@1.14.0, @mastra/server@1.81.0

  • Added getMcpServerTasks(), findMcpServerTask(), cancelMcpServerTask() and answerMcpServerTaskInput() for an MCP server's tasks. (#28629)

    const { tasks, nextCursor } = await client.getMcpServerTasks('reports', { status: 'input_required' });
    await client.answerMcpServerTaskInput('reports', tasks[0].taskKey, {
      approve: { action: 'accept', content: { ok: true } },
    });
  • Added outcome and stopReason filters and groupings to querySpans(), aggregateSpans(), queryTraces(), and aggregateTraces(), with outcome, stopReason, and stopReasonContext on results. Check spanQueryOutcomeFields and traceQueryOutcomeFields from getObservabilityCapabilities() first: (#28884)

    const { capabilities } = await client.getObservabilityCapabilities();
    if (capabilities.spanQueryOutcomeFields) {
      await client.querySpans({ timeRange, where: { op: 'eq', left: { path: 'outcome' }, right: { literal: 'limit' } } });
    }

Patch Changes

  • Exported the saved view and filter types (SavedViewState, FilterNode and related types) from @mastra/client-js. (#28715)

  • getObservabilityCapabilities() returns the spanAggregateModelDimensions flag. (#28908)

  • getObservabilityCapabilities() returns the spanQueryModelFields flag. (#28859)

  • Added the dashboard definition types (DashboardDefinition, DashboardWidget, DashboardQuery, and related types) to the exports. (#28714)

  • queryTraces(), querySpans() and queryTraceThreads() results now include timeToFirstTokenMs when the store supports it, and queryTraces() and querySpans() accept it in orderBy. (#28699)

@mastra/cloudflare@1.8.0

Minor Changes

  • Added storage for the optional background task fields expiresAt and metadata, plus filtering by expiresAt. Existing tables and collections gain the new fields automatically when the store initializes, and existing tasks keep using the global retention settings. Requires the matching @mastra/core release. (#28425)

    Convex users must redeploy their Convex schema after upgrading, because the adapter now writes the expiresAt and metadata fields.

    Also in: @mastra/cloudflare-d1@1.5.0, @mastra/convex@1.8.0, @mastra/dynamodb@1.5.0, @mastra/lance@1.6.0, @mastra/libsql@1.28.0, @mastra/mongodb@1.25.0, @mastra/mssql@1.11.0, @mastra/mysql@0.15.0, @mastra/pg@1.34.0, @mastra/upstash@1.6.0

Patch Changes

  • Fixed a late background task write overwriting a newer suspension. (#28590)

    Also in: @mastra/cloudflare-d1@1.5.0, @mastra/convex@1.8.0, @mastra/dynamodb@1.5.0, @mastra/lance@1.6.0, @mastra/libsql@1.28.0, @mastra/mongodb@1.25.0, @mastra/mssql@1.11.0, @mastra/mysql@0.15.0, @mastra/pg@1.34.0, @mastra/spanner@1.11.0, @mastra/upstash@1.6.0

  • Fixed upgraded D1 and Durable Object stores deleting finished background tasks early. (#28911)

    Also in: @mastra/cloudflare-d1@1.5.0

@mastra/code-sdk@1.17.0

Minor Changes

  • Added control over rich scorer-result storage. Hosts can retain the safe default or enable their configured storage for Studio score access. (#28873)

    createMastraCode({ disableScoreStorage: false });

Patch Changes

  • Export CODE_AGENT_ID from @mastra/code-sdk/constants, the id of the coding agent Mastra Code registers. (#28604)

  • Fixed OpenAI Codex (ChatGPT sign-in) threads failing with Item with id 'rs_…' not found after sitting idle. (#28786)

  • Mastra Code now closes its browser when it exits, when it relaunches, and when /browser replaces or disables it, so a leftover Chrome no longer keeps the browser profile locked. (#28675)

  • Mastra Code file edit and write tools no longer fail when a file was read or changed outside the view tool (for example with cat, sed -i or a formatter). (#28935)

  • Fixed Factory sessions failing with a sign-in error when a provider such as OpenAI Codex or Anthropic rejects a personal OAuth token before its saved expiry. (#28904)

    Also in: @mastra/factory@0.26.0

@mastra/connect@1.3.0

Minor Changes

  • Added a Microsoft Excel toolset with 27 tools for drive discovery, workbook files, worksheets, ranges, tables, names, charts, and search. (#26283)

    import { Agent } from '@mastra/core/agent';
    import { tools } from '@mastra/connect';
    
    const excelAgent = new Agent({
      id: 'excel-agent',
      name: 'Excel agent',
      instructions: 'Use the connected Microsoft Excel tools to manage workbooks.',
      model: 'openai/gpt-5-mini',
      tools: tools({ providers: ['microsoft-excel'] }),
    });
  • Added a Microsoft Word toolset with 18 tools for drive discovery, document creation, copying, updating, deletion, moving, sharing, and export. (#26282)

    import { Agent } from '@mastra/core/agent';
    import { tools } from '@mastra/connect';
    
    const wordAgent = new Agent({
      id: 'word-agent',
      name: 'Word agent',
      instructions: 'Use the connected Microsoft Word tools to manage documents.',
      model: 'openai/gpt-5-mini',
      tools: tools({ providers: ['microsoft-word'] }),
    });
  • Added a Microsoft Outlook toolset with 30 tools for messages, replies, drafts, attachments, mail folders, calendars, and events. (#26281)

    import { tools } from '@mastra/connect';
    
    const outlookTools = await tools({ providers: ['outlook'] })();
    const inbox = await outlookTools.outlook_list_messages.execute({ folderId: 'inbox', limit: 10 });
  • Added a SharePoint Online toolset with 53 tools for sites, lists, list items, columns, content types, pages, permissions, drives, and drive items with binary content download. (#26284)

    import { Agent } from '@mastra/core/agent';
    import { tools } from '@mastra/connect';
    
    const sharepointAgent = new Agent({
      id: 'sharepoint-agent',
      name: 'SharePoint agent',
      instructions: 'Use the connected SharePoint Online tools to manage sites and lists.',
      model: 'openai/gpt-5-mini',
      tools: tools({ providers: ['sharepoint-online'] }),
    });

Patch Changes

  • Fixed provider generation so aliased integration templates can be discovered and added with add-provider. (#26282)

  • Fixed delegated Microsoft Word, Excel, and PowerPoint drive discovery so get-user-drive defaults to the connected user's OneDrive, while regenerated tools retain supported request and UUID behavior. (#26282)

  • Fixed two problems in provider tool requests. (#26283)

@mastra/daytona@0.11.5

Patch Changes

  • Updated @daytonaio/sdk to ^0.226.1. (#28975)

@mastra/deployer@1.81.0

Patch Changes

  • mastra dev and mastra start now run the server with Node's --enable-source-maps flag when bundler.sourcemap is enabled, so stack traces point at your source files instead of .mastra/output/index.mjs. (#29054)

  • Fixed bundler options in src/mastra/index.ts being ignored when a project has file-based agents (src/mastra/agents/<name>/). (#29054)

  • Improved compatibility between @mastra/deployer deployers and Mastra: the Bundler and Deployer base classes no longer extend @mastra/core's MastraBundler at build time; they implement the IBundler/IDeployer interfaces instead. (#28428)

@mastra/deployer-cloud@1.81.0

Patch Changes

  • Fixed the minimum @mastra/core peer dependency to >=1.81.0-0 <2.0.0-0. (#28428)

    Also in: @mastra/deployer-cloudflare@1.2.40, @mastra/deployer-netlify@1.2.40, @mastra/deployer-sandbox@0.3.25, @mastra/deployer-vercel@1.2.40, @mastra/temporal@0.4.19

@mastra/duckdb@1.17.0

Minor Changes

  • DuckDB trace, span and thread queries now return and filter by timeToFirstTokenMs, and trace and span queries can sort by it, slowest or fastest first. Traces use the root span's value. Traces and spans without a value come last. (#28699)

    orderBy: [{ field: 'timeToFirstTokenMs', direction: 'desc' }];

@mastra/e2b@0.16.0

Patch Changes

  • Updated sandbox provider SDKs to their latest releases: e2b to ^2.61.0, modal to ^0.11.1 and @vercel/sandbox to 2.9.2. (#28975)

    Also in: @mastra/e2b-desktop@0.1.11, @mastra/modal@0.7.4, @mastra/platform-workspace@1.11.0, @mastra/vercel@1.6.3

@mastra/elysia@0.1.21

Patch Changes

  • The Elysia and Koa adapters no longer log 4xx errors thrown by route handlers as server errors, matching the Hono, Express, and Fastify adapters. (#28925)

    Also in: @mastra/koa@1.7.25

@mastra/evals@1.10.10

Patch Changes

  • Fix tool-call accuracy scores to account for missing and repeated calls even when the judge returns fewer evaluations than actual tool calls. (#28771)

@mastra/factory@0.26.0

Minor Changes

  • Board phases can now say who runs them and when they start. defineBoard() accepts name, description and nextPhases, and working phases accept a runner ({ agentId, skill, context, instructions }), autoStart with an optional autoStartWhen rule, and an exitTool that ends the phase: once it succeeds, the agent moves the card to one of nextPhases with factory_transition_work_item. autoApprove and autoApproveWhen let a phase accept a submit_plan plan without a person. (#28604)

    planning: {
      name: 'Planning',
      kind: 'working',
      runner: { context: 'build', skill: 'plan' }, // a skill that ends by calling submit_plan
      autoStart: true,
      autoStartWhen: LOW_EFFORT,
      exitTool: { id: 'submit_plan', autoApprove: true, autoApproveWhen: LOW_EFFORT },
      nextPhases: ['execute', 'canceled'],
    },

    Existing board declarations keep working: defineBoard() maps the deprecated title, role and next to name, runner.context and nextPhases and logs a warning listing them. The board it returns has only the new fields, so code reading board.title, phase.title or phase.role must read name and runner.context instead. The board catalog keeps sending title and role for older clients. It also sends each working phase's autoStartWhen and exitTool.autoApproveWhen, so a client can tell which cards a phase starts or approves on its own.

    The built-in Work and Review boards now use the new fields, with nextPhases instead of outcomes.

    Integration rule contexts describe the card's phase with nextPhases and context beside outcomes; role stays as a deprecated alias of context. Built-in phases declare no outcome names, so rules that tested context.phase?.outcomes should test context.phase?.nextPhases.

  • Custom boards can now run your own skills. Pass skills to new MastraFactory({ skills }), or add factory-skills/<name>/SKILL.md to your app, and name one in a phase's runner.skill. Your skill replaces a built-in skill of the same name. (#28823)

    import { createSkill } from '@mastra/core/skills';
    
    const releaseNotes = createSkill({
      name: 'release-notes',
      description: 'Draft release notes for a work item',
      instructions: 'Group the release notes by package and end with a one-line summary.',
    });
    
    const notesBoard = defineBoard({
      id: 'notes',
      name: 'Notes',
      initialPhase: 'backlog',
      phases: {
        backlog: { name: 'Backlog', kind: 'resting', nextPhases: ['drafting'] },
        drafting: { name: 'Drafting', kind: 'working', runner: { skill: 'release-notes' }, nextPhases: ['published'] },
        published: { name: 'Published', kind: 'terminal' },
      },
    });
    
    new MastraFactory({ ...config, boards: [notesBoard], skills: [releaseNotes] });

    Settings › Skills now lists your app's skills and shows the skill each custom phase runs. The board catalog sends each working phase's skill. The constructor warns when a phase names a skill that does not exist, or runs a review skill outside the review context.

  • Factory app skills can live in src/mastra/factory-skills/<name>/SKILL.md, next to the rest of your Mastra code, instead of under src/mastra/public/factory-skills. mastra dev reads the folder directly and mastra build copies it into the build output. The public/ folder still works; when both exist, src/mastra/factory-skills wins. (#28838)

    const releaseBoard = defineBoard({
      id: 'release',
      initialPhase: 'backlog',
      phases: {
        backlog: { name: 'Backlog', kind: 'resting', nextPhases: ['drafting'] },
        drafting: {
          name: 'Drafting',
          kind: 'working',
          // Runs src/mastra/factory-skills/release-notes/SKILL.md
          runner: { context: 'writer', skill: 'release-notes' },
          nextPhases: ['published'],
        },
        published: { name: 'Published', kind: 'terminal' },
      },
    });
  • Organization admins can now create, edit and delete a project's own boards through the API. Saved boards pass the same checks as defineBoard(), and every change sends back the revision it read, so two people can't overwrite each other's edits. (#28878)

    curl -X POST "$FACTORY_URL/web/factory/projects/$PROJECT/boards" \
      -H 'content-type: application/json' \
      -d '{ "key": "bugs", "name": "Bugs", "phases": [{ "key": "reported", "name": "Reported", "kind": "resting" }, { "key": "fixed", "name": "Fixed", "kind": "terminal" }] }'

    GET /web/factory/projects/:id/boards/:boardKey returns a board's definition, PATCH …/boards/:boardKey replaces it with expectedRevision, and DELETE …/boards/:boardKey?expectedRevision=n deletes it. Factory refuses edits that would strand cards (409 phase_in_use, 409 board_in_use) and runner skills that don't exist (422 unknown_skill).

    The built-in Work and Review boards and boards passed to new MastraFactory({ boards }) are read-only here: creating, editing or deleting one returns 409 code_board, and the catalog and GET report them with editable: false.

    defineBoard() and the boards and includeDefaultBoards options of MastraFactory are deprecated. They keep working, their boards are not stored, and Factory logs one deprecation warning per process. Apps that use only the built-in boards see no warning. See the "Stored boards" and "Migrating from defineBoard()" sections of the README.

  • Each project's boards are now the built-in Work and Review boards and the boards you pass to new MastraFactory({ boards }), followed by any boards the project created for itself, which are stored in its database. Code boards are served exactly as your code defines them, are never written to the database, and are read-only. When a stored board has the same key as a code board, the code board wins. (#28877)

    Existing apps need no change, and Factory writes nothing to the database at startup.

    The board catalog (GET /web/factory/projects/:id/boards) now reports each board's source (builtin, app or stored), whether it is editable (false for code boards) and any problems, such as a runner skill that does not exist.

  • Added opt-in rich score persistence for Factory deployments so trace scores can appear in Mastra Studio. Storage remains disabled by default to avoid unbounded score growth. (#28873)

    new MastraFactory({ storage, disableScoreStorage: false });
  • Added label filters to Jira intake routing, so one Jira project can feed several factories. Each factory's binding takes either all issues or only issues carrying its labels. Label filters take precedence over the all-issues binding; an issue carrying labels from two filters goes to the binding created first. Each issue is filed in exactly one factory. (#28605)

    PUT /web/intake/bindings
    { "integrationId": "jira", "sourceId": "10000", "factoryProjectId": "<payments factory>", "board": "work", "labels": ["payments"] }
    
    DELETE /web/intake/bindings?integrationId=jira&sourceId=10000&factoryProjectId=<payments factory>

    Overlapping labels, or a second all-issues binding, are rejected with 409 binding_conflict. Labels decide placement only when an issue is first filed; relabelling an issue does not move its card. Linear, GitHub, GitLab and incident.io bindings are unchanged: a source still feeds one factory.

    Upgrading: existing bindings move to new storage on first start and keep taking all issues, so nothing changes until you add a label filter. If you roll back to an earlier version, bindings return to how they were when you upgraded; changes made since, including label filters, are not carried back.

  • Added custom model provider creation during Factory setup, with immediate provider and model catalog refresh for organization and personal flows. (#28706)

  • Planning and Building on the Work board now run in one work session, so the builder keeps the plan's reasoning, branch and sandbox. When the planner moves the card to Building, the build prompt is queued onto its own run. (#28721)

    This replaces the fallback that let a build borrow the plan or triage session. Triage no longer hands its session to later phases. Runs and API calls that name the plan role still start Planning, in a plan session the build then continues in; cards planned before this change keep using their plan session the same way. Self-review keeps its own review session.

    invokeSkill decisions accept startsAfterOwnMove: true to start a run even when the agent of the same session moved the card. Custom board phases with a runner.skill set it when a card enters from another phase, so moving a card between phases that share a context now starts the next phase's skill.

    phases: {
      building: {
        kind: 'working',
        name: 'Building',
        runner: { context: 'builder' },
        onEnter: {
          issue: context => ({
            type: 'invokeSkill',
            role: 'builder',
            idempotencyKey: `${context.ingress.id}:build`,
            prompt: 'Build the approved plan.',
            // The planner shares this session and moved the card itself; start the build anyway.
            startsAfterOwnMove: true,
          }),
        },
      },
    },

Patch Changes

  • Made the work board's polling cheaper on large projects. (#28889)

  • Restored title, role and next on boards returned by defineBoard(). (#28914)

  • Fixed three Jira intake routing problems when factories share a Jira project: (#28870)

  • The latest account-related failure card in the Factory web chat now names the credential the session runs on. (#28704)

  • The activity line under the transcript now follows the whole run instead of disappearing at the first output. (#28834)

  • Anonymous callers can no longer browse files, list artifacts or resolve codebase paths in a Factory deployment with authentication enabled; those routes now require a signed-in user. (#28829)

  • A reply's footer (model, copy button and time) now appears once the last word of the whole reply is on screen, including earlier steps of a reply split across several messages. (#28834)

  • More failed-run cards in the Factory web chat now come with a button that fixes them: (#28695)

  • Updated Factory's browser tab icon to use the same black tile as Studio and Platform in both light and dark mode, including the session status icons. (#28840)

  • Signal, system reminder and skill rows in the transcript now read like the agent's activity lines: same left edge, same type and weight, a readable name instead of the signal's id, and the message as prose instead of monospace. (#28834)

  • Factory triage now handles Jira, incident.io, and GitLab cards. (#28775)

  • Factory no longer reports a stalled sandbox as "git is not installed". (#28792)

  • Linking a GitHub repository to a Factory is faster: it now looks up the one selected repository instead of listing every repository the GitHub App can see. (#28551)

  • Fixed Factory intake losing or reviving routing in edge cases: (#28971)

  • Browser screenshots now show as images in the session transcript instead of their raw base64 data. (#28503)

  • Brought back the + next to Sessions in the Factory sidebar, beside the properties menu. (#28761)

@mastra/fastembed@1.3.6

Patch Changes

  • Fixed custom FastEmbed models failing to load when tokenizer_config.json stores pad_token as a serialized AddedToken object (as exported by older Transformers versions). (#28893)

  • Direct FlagEmbedding and SparseTextEmbedding calls now throw a clear error when a custom ONNX model returns a different number of output rows than input texts, instead of returning the wrong number of embeddings or failing with an unrelated TypeError. (#28892)

@mastra/inngest@1.11.0

Minor Changes

  • Added execution outcomes and stop-reason details to Inngest agent and workflow traces. Separate resumed-segment tracing and termination-cause preservation across workers are not yet supported; missing causes can produce unknown outcomes. A run deadline received by the caller may not be recorded as limit / timeout when another worker owns finalization. (#28703)

Patch Changes

  • Inngest durable agents now report time to first token on their generation and agent run spans. (#28699)

@mastra/koa@1.7.25

Patch Changes

  • Koa no longer prints 4xx errors or 501 Not Implemented errors to console.error. (#28925)

@mastra/lance@1.6.0

Patch Changes

  • Fixed the LanceVectorStore.create() type so id is optional when you pass connection options. (#28969)

@mastra/langsmith@1.4.1

Patch Changes

  • Spans keep timeToFirstTokenMs out of LangSmith metadata, as with completionStartTime, which already drives the new_token event. (#28699)

@mastra/livekit@0.5.3

Patch Changes

  • Fixed a build failure when @livekit/protocol resolves to a different version than the one livekit-server-sdk depends on. (#28672)

@mastra/mcp@2.7.0

Minor Changes

  • Added Mastra-storage-backed tasks to MCPServer (experimental). Closes #28337. (#28521)

    • Shared tasks. A server registered on a Mastra instance with background tasks enabled keeps its tasks in Mastra storage. Any instance that shares the storage can check, answer, cancel, and run a task.
    • Survives restarts. Answers outlive the instance that took them. Each instance picks up tasks left by stopped instances as soon as the server is registered.
    • Cleans up after itself. Expired tasks fail, and finished tasks are deleted when their TTL ends.
    • Limits apply everywhere. maxOpenTasksPerClient and maxOpenTasks count tasks across all instances.
    • Private by default. Storage never keeps task ids or bearer tokens. A readable metadata.clientLabel shows which client sent a task.
    • One trace per task. The call that creates a task, each run of the tool, and later answers and cancels share one trace. Polls aren't traced.
    • Opt-in recovery. When the instance running a task stops, the task fails by default. Set recover: true to run the tool again on another instance. Only use it for tools that are safe to repeat.
    const server = new MCPServer({
      id: 'reports',
      name: 'Reports',
      version: '1.0.0',
      tools: { buildReport },
      tasks: { tools: { buildReport: { mode: 'always', recover: true } } },
    });
    
    export const mastra = new Mastra({
      storage,
      backgroundTasks: { enabled: true },
      mcpServers: { reports: server },
    });
  • MCP tasks in Studio (#28629)

    MCP servers with tasks enabled now report which tools run as tasks (always or auto, whether clients must support tasks to call them, and whether they recover after a restart), and give Studio an admin view to list tasks (filtered by status, tool or client, page by page), find one by its task id, and cancel or answer them. Cancelling or answering from Studio goes through the same path as the protocol's tasks/cancel and tasks/update. The view shows hashed task and client keys and a readable client label, never raw task ids or credentials. Custom task stores can opt in by implementing MCPTaskStoreAdmin (list, updateByKey, cancelByKey); stores without it are reported as not listable.

    The MCP client now writes an mcp-task progress entry (MCPTaskProgress) to a tool's output stream when its call continues as an MCP task and whenever the task's status changes, so UIs can show the remote task id and status while it runs.

    import type { MCPTaskStore, MCPTaskStoreAdmin } from '@mastra/mcp';
    
    // A custom store opts into Studio's task list by also implementing MCPTaskStoreAdmin.
    class MyTaskStore implements MCPTaskStore, MCPTaskStoreAdmin {
      // ...the MCPTaskStore methods, plus:
      async list(options) {
        /* newest first, with nextCursor when more match */
      }
      async updateByKey(taskKey, inputResponses) {
        /* 'done' | 'ignored' | 'not_found' */
      }
      async cancelByKey(taskKey) {
        /* 'done' | 'ignored' | 'not_found' */
      }
    }
  • Added experimental support for answering tool calls with MCP Tasks (io.modelcontextprotocol/tasks). A long-running tool can now hand the client a task right away; the client polls for the result, so the work isn't limited by HTTP timeouts. Clients that don't support tasks keep getting normal answers. (#28420)

    const server = new MCPServer({
      name: 'Reports',
      version: '1.0.0',
      tools: { buildReport, lookupCustomer },
      tasks: {
        tools: {
          buildReport: 'always',
          lookupCustomer: { mode: 'auto', syncWindowMs: 1000 },
        },
      },
    });

    Tasks are bound to the caller that created them, can be cancelled (which aborts the tool's abortSignal), and expire after 24 hours by default. A tool that calls suspend() inside a task asks the client for input, and runs again with resumeData once the client answers. Tasks are kept in memory by default; pass tasks.store to keep them elsewhere. Each client can hold up to 100 open tasks and the server up to 1000 (tasks.maxOpenTasksPerClient, tasks.maxOpenTasks). Past that, an 'always' tool answers with an error and an 'auto' tool answers synchronously. The in-memory store also caps how many tasks it keeps, finished ones included, at 1000 per client and 10000 in total (tasks.maxRetainedTasksPerClient, tasks.maxRetainedTasks), refusing new tasks the same way.

    Task runs use the request context stored when the task was created, with the bearer token removed, and get no context.mcp. The only exception is the first run of an 'auto' tool, which started under the request. Read cancellation from context.abortSignal.

  • Added MCP tasks for workflows and agents served by MCPServer (experimental). The tasks.workflows and tasks.agents options let run_<workflow> and ask_<agent> answer with a task, using the same modes as tools. (#28619)

    const server = new MCPServer({
      id: 'reports',
      name: 'Reports',
      version: '1.0.0',
      workflows: { nightly },
      agents: { researcher },
      tasks: {
        workflows: { nightly: 'always' },
        agents: { researcher: { mode: 'auto', syncWindowMs: 1000 } },
      },
    });

    Workflows: a workflow run starts once per request, and the task follows that run. A suspended step with a resumeSchema the client can fill in asks for input through the task, and the answer resumes the run. Other suspended steps leave the task working until the run is resumed some other way. Cancelling the task cancels the run, even while it waits for input. A run that fails completes the task with isError: true. With tasks kept in Mastra storage under a stable MCPServer id, when a server stops mid-run, Mastra's workflow recovery continues the run on the next instance and the task follows it. Tasks kept in memory end with the server.

    Agents: an agent task runs the agent's generate() call as the task's work. Cancelling the task aborts that call, and agent tasks support recover. An agent that pauses for tool approval completes the task with an error, since tasks can't answer approvals yet.

    tasks.default now also applies to workflows and agents not listed in tasks.workflows or tasks.agents, and tasks.required takes the names clients call, such as run_nightly.

  • Added experimental support for MCP tasks in MCPClient. A server can now answer a long-running tool call with a task instead of holding the request open. Mastra waits for the task and returns its final result, so tool calls keep the same shape. (#28343)

    Turn it on per server:

    const mcp = new MCPClient({
      servers: {
        reports: {
          url: new URL('https://example.com/mcp'),
          tasks: { maxWaitMs: 30 * 60_000 },
          inputRequests: async ({ params }) => ({ action: 'accept', content: { ok: true } }),
        },
      },
    });
    • A task that needs input is answered through your inputRequests handler and sent back with tasks/update, without calling the tool again.
    • Aborting the tool call or reaching maxWaitMs cancels the remote task and ends the call right away, even while a poll or your input handler is still running. A cancelled or failed task becomes a tool error.
    • Polling rides out a server restart or deploy: the client reconnects and retries with backoff for up to 5 minutes before failing the call.
    • On an agent with background tasks enabled, these tools run as background tasks, so the agent keeps working while the remote task runs.
    • The tool call span carries mcpTaskKey metadata (a hash of the task id) to match it with the server's spans for that task.
    • mcp.tasks.get, mcp.tasks.update and mcp.tasks.cancel read, answer and cancel a task directly.

    Tasks are off unless tasks is set, so existing servers behave as before. See #26000.

Patch Changes

  • Fixed MCP task tool calls failing when a server sends a valid ISO 8601 createdAt that JavaScript can't parse, such as 20261029T120000Z. (#28899)

@mastra/memory@1.40.0

Minor Changes

  • Added compact observer failure details and a reference to the failed memory-operation span when Observational Memory interrupts an agent. Observer context-window exhaustion is recorded as a limit, while the affected agent records an interruption. (#28703)

    Failure markers in streams and persisted messages, and tripwire metadata, now include observerFailure with the observer span reference and failure classification.

Patch Changes

  • Deleting a thread with memory.deleteThread() now also deletes the thread's live event stream from the Mastra pubsub (for example, Redis). (#28968)

  • Fixed observational memory search scanning the whole vector table on PostgreSQL. (#29058)

  • Fixed observational memory re-embedding every earlier observation group on each cycle when vector retrieval is enabled. (#28150)

@mastra/observability@1.23.0

Minor Changes

  • Added execution outcomes and stop-reason details to span lifecycle methods and both Mastra exporters. Mastra-generated stop-reason context contains compact facts and span references; custom context is copied and safely serialized before export. (#28703)

    Custom producers retain support for arbitrary outcome strings and context objects, including when spans are rebuilt. Framework-generated context omits raw signals, tripwire payloads, and other unbounded diagnostic data. Model chunk content remains downstream of output processors.

    Custom spans can record how they ended:

    span.end({ outcome: 'limit', stopReason: 'max_steps', stopReasonContext: { maxSteps: 5 } });

    To end a different span and its open descendants after cancellation:

    runSpan.end({ endTree: true, outcome: 'abort', stopReason: 'caller_cancelled' });

    Processed MODEL_CHUNK spans are now children of MODEL_STEP, independent of output-processor buffering. Agent-stream MODEL_INFERENCE spans retain provider timing and usage but no longer record response output; use processed chunks or MODEL_GENERATION output instead. Public native outcome fields also allow null, which exporter consumers must handle.

    Ended spans ignore subsequent end(), error(), and outcome updates, preserving the exported result. A null or absent outcome means no outcome was recorded; use endTime to determine completion.

  • Exported spans now include model and provider when the span's own attributes have both. Storage and exporters receive them as top-level fields. An agent run carries the model it started with, and model steps and inferences carry the model that each call used. (#28849)

  • Model and agent spans now report timeToFirstTokenMs: the milliseconds from the start of the model call to its first streamed chunk. Unlike timeToFirstChunkMs, it leaves out the work Mastra does before calling the model, such as input processors. (#28699)

    The value is measured once on the model inference span that streamed the chunk, and model step, model generation and agent run spans carry the value of the nearest such inference below them, as a timeToFirstTokenMs attribute. Spans that never streamed a chunk have no value. A fallback model attempt now opens its own model inference span, named after the model it calls, so its value and label are its own, and the failed attempt's span ends with the error. When an in-process agent retries a failed model call, the retry happens inside one inference span, so the value includes the failed attempt and the retry wait.

Patch Changes

  • Update bundled model pricing with the latest prices from models.dev (9f03685)

@mastra/otel-exporter@1.5.0

Minor Changes

  • Added explicit custom.endpoints.traces and custom.endpoints.logs URLs, passed through without rewriting paths or query parameters. Omitted signals are not exported. Configure either endpoint or endpoints, never both; this is enforced in TypeScript and at runtime. The singular endpoint option retains its legacy behavior. (#28820)

    Added opt-in metadataMapping to copy selected metadata into top-level span attributes. Existing exports remain unchanged without a mapping:

    new OtelExporter({
      provider: {
        custom: {
          endpoints: {
            traces: 'https://collector.example.com/api/otel/traces',
            logs: 'https://collector.example.com/events',
          },
        },
      },
      metadataMapping: { userId: 'user.id', tenantId: 'tenant.id' },
    });

    Mapped values are strings; objects and arrays are JSON-encoded. Namespaced mastra.metadata.* attributes are retained.

@mastra/pg@1.34.0

Minor Changes

  • PostgreSQL trace, span and thread queries now return and filter by timeToFirstTokenMs, and trace and span queries can sort by it, slowest or fastest first. Traces use the root span's value. Traces and spans without a value come last. (#28699)

    orderBy: [{ field: 'timeToFirstTokenMs', direction: 'desc' }];

Patch Changes

  • Fixed PgVector.query() skipping HNSW indexes when a filter or minScore was set (#28949). (#29058)

  • Experiments and their results now keep the run times they were given. (#28836)

  • Fixed PostgresStoreVNext observability dropping every span, metric, log, score, and feedback event once the day partitions created by init() ran out. (#28680)

  • Fixed PgVector.createIndex() timing out with metadataIndexes when the connection pool has a single connection (pgPoolOptions: { max: 1 }). (#29058)

  • Fixed a startup crash when multiple processes, such as an API and a worker, initialize the same empty Postgres schema at the same time. (#28670)

@mastra/playground-ui@67.0.0

Minor Changes

  • ActivityStretchLine now shows the AgentIndicator in place of its icon. While its tool calls run, the indicator takes the shape of the call in progress (a terminal for a command, an eye for a read, a magnifier for a search), then rests as the agent icon once they finish. A line that is already finished when it mounts draws a still icon instead. (#28834)

    A live line draws before its first row lands. Its new current prop (ActivityCallState[]) lets the line name the running call while that call's row is still held back. The line reads in medium weight and no longer draws a rule under itself, so consecutive activity lines keep one rhythm.

    A live line writes its first words in from the left. When what it says changes, the old words roll up and blur away while the new ones roll into place, and the line glides to its new width. A line that is already finished when it mounts never moves, and opening one shows its rows without fading them in. Words too long for the line fade out at its right edge instead of ending in an ellipsis.

    Only a top-level activity line keeps its disclosure chevron visible. Rows nested inside another activity show theirs on hover or keyboard focus, and take no room until then.

  • Activity details now read in the body font at the label's size, so a row reads as one phrase. ActivityDetail no longer takes font, and ActivityHeadline and ActivityItem no longer take detailFont. Drop the prop: (#28834)

    // Before
    <ActivityDetail font="mono">{path}</ActivityDetail>
    <ActivityItem {...item} detailFont="mono" />
    
    // After
    <ActivityDetail>{path}</ActivityDetail>
    <ActivityItem {...item} />

    Wrap a transcript in ActivityWorkingDirectory to show paths under the agent's working directory relative to it, both on screen and in the row's accessible name:

    import { ActivityWorkingDirectory } from '@mastra/playground-ui/components/ai/activity';
    
    <ActivityWorkingDirectory path="/sandboxes/7b45/mastra">{transcript}</ActivityWorkingDirectory>;
    // "/sandboxes/7b45/mastra/packages/core/package.json" reads "packages/core/package.json"

    Use useRelativePaths() to rewrite your own text the same way.

  • Added AgentIndicator, an animated glyph that shows what an agent is doing. It rests as the agent icon and morphs between one pose per kind of work: idle, memory, thinking, reading, searching, finding, browsing, editing, running, deleting, skill, tool, waiting, done, error, context and notice. It draws in currentColor, sizes with className, and holds still when the user prefers reduced motion. Pass still to keep idle and waiting from breathing when the indicator marks something that is over. (#28834)

    import { AgentIndicator, agentGlyph } from '@mastra/playground-ui/components/AgentIndicator';
    
    <AgentIndicator state="running" className="size-4" />;
    
    const RunGlyph = agentGlyph('running');
    <RunGlyph className="size-4" />;

    agentGlyph(state) returns any pose as a still, Lucide-compatible icon, drawn from the same frames the indicator morphs through. Tool, signal, skill, reasoning and memory rows now use these glyphs, so a row's icon and the live indicator always show the same shape. presentTool returns the call's pose next to its icon.

  • Added Continuing and Unwatched to the arrival primitives. Continuing moves something the reader already sees into a new place without fading it in, and cancels the fade-in of any arriving element around it. Unwatched marks content whose fade-in its parent plays, so nothing inside it fades in on its own. A Shimmer inside another Shimmer now keeps its own text visible while it moves. (#28834)

    import { Continuing, Unwatched } from '@mastra/playground-ui/components/Arrival';
    
    // The run's indicator steps from the tail onto the line that now describes it, without fading in again
    <Continuing>
      <ActivityStretchLine members={members} live />
    </Continuing>;
    
    // A folded body whose disclosure plays the entrance for everything inside it
    <Unwatched>{body}</Unwatched>;
  • Unified metrics chart card headers on one style: a medium-weight title with the card's total on the right. Descriptions, labelled summaries, card tabs and numbers in chart legends are gone; a card with several views switches them from its title. (#28860)

    Breaking: removed

    • ChartCard props description and summary. Use total (and isLoading).
    • MetricsCard.Description, MetricsCard.TitleAndDescription, MetricsCard.Summary, MetricsCard.Tabs, MetricsCard.Tab, and MetricsCard.Title's weight prop (titles are always medium).
    • aggregate on MetricsLineChartSeries and the data prop of MetricsLineChartLegend.
    • MetricsKpis.
    // Before
    <ChartCard title="Latency" description="Duration percentiles." summary={<MetricsCard.Summary value="4.6s" label="P95" />}
      toolbar={<MetricsCard.Toolbar><MetricsCard.Tabs value={view} onValueChange={setView}>…</MetricsCard.Tabs></MetricsCard.Toolbar>}>
    
    // After
    <ChartCard title="Agent latency" total="4.6s"
      titleMenu={{ value: view, onValueChange: setView, options: [{ value: 'agents', label: 'Agent latency' }, …] }}>

    Added

    • ChartCard variant="hero" and MetricsCard.Headline, for one large chart that is the page's subject: a label over a large total, the change against the previous period beside it, and controls on the right.
    • total can take one value per series ([{ label, value, color }]), each after a marker in its series' color, for a card charting a few different things. Resources and threads uses it.
    • AgentRunsCard switches to Failure rate from its title (onViewErrors, onFailureTimeRangeClick).

    Changed

    • Chart cards in a grid row end at the same height, and their charts line up in every state (loading, empty, error).
    • On ranges that start and end at the same time of day (last 24 hours, 3 days, 7 days), small charts label their ends by date alone ("Oct 28", "Oct 29"), and those labels reach the plot's edges.
    • A chart's tooltip stays above the chart cards that follow it on the page.
    • "View in traces" buttons use the Traces icon, card action buttons are smaller and line up with the title, and a card's title menu closes once a view is picked.
    • A series with a single point gets a dot; other series draw lines only.
  • Added Outcome and Stop reason columns to the Traces and Spans tables in Studio. Outcome shows how the run ended (for example Limit), and Stop reason shows why (for example max_steps), with the stop reason context on hover. Rows without a recorded outcome show their error-based status in the usual colors, with a note that the outcome was not recorded. (#28881)

    Added Outcome and Stop reason filters to the Traces and Spans filter bars, including an "is not recorded" option for runs without an outcome. The filters appear only when the storage can filter on these fields.

    Filter bar fields can now relabel an operator with operatorLabels, for example { notExists: 'is not recorded' }.

  • Added helpers that turn an observability list page's URL into a saved view and apply a saved view back to the URL (savedViewFromSearchParams, applySavedView). Parts of a view the page no longer supports, such as a removed filter field or column, are dropped one by one and the rest still applies. The spans and threads pages now keep their time range, filters and sort in the URL, so they survive a reload and can be shared. (#28715)

    import {
      applySavedView,
      savedViewFromSearchParams,
    } from '@mastra/playground-ui/domains/observability/saved-view-state';
    
    // Save the current page state as a view.
    const view = savedViewFromSearchParams('spans', new URLSearchParams(window.location.search));
    
    // Later, apply it: `params` is the URL to navigate to.
    const { params } = applySavedView(view, new URLSearchParams(window.location.search));
  • SignalActivity and SkillMessage take appearance="inline" to read like the agent's own activity lines. An inline signal is one line: its icon, a readable name (factory-environment reads as "Factory environment"), and the mode in plain words. It opens onto the message as prose, with line breaks kept and never in monospace, and a message short enough to fit stays on the line, wrapping on a narrow screen instead of being cut off. ActivityLabel takes wrap for that. (#28834)

    <SignalActivity
      kind="state"
      label="factory-environment"
      mode="snapshot"
      message="1 repository at /workspace"
      appearance="inline"
    />
  • Added a full-width histogram of counts and errors over time to the Traces and Spans pages, between the filter bar and the list. It follows the list's filters and time range, and only appears when the server supports trace or span aggregates. (#28816)

    Drag across the histogram to zoom in: the list and the chart switch to a custom time range covering the selected bars. MetricsStackedBarChart exposes this as an opt-in onZoom({ start, end }) prop that reports the first and last selected column.

    Other charts can reuse the same drag-to-select behavior from @mastra/playground-ui/primitives/charts/drag-selection. It works with bar and line charts (pass layout="point" to DragSelectionBand for line charts).

    const selection = useDragSelection({ onSelect: ({ start, end }) => zoomTo(start, end) });
    
    <ComposedChart
      className={selection.className}
      onMouseDown={selection.onMouseDown}
      onMouseMove={selection.onMouseMove}
      onClick={state => {
        if (selection.consumeReleaseClick()) return;
        // regular point click
      }}
    >
      <DragSelectionBand selection={selection} count={data.length} />
      {/* series */}
    </ComposedChart>;

    When the Traces, Spans, or Threads time range differs from the default, the Time chip shows a × that resets it to the last 7 days. TraceTimeRangeChip exposes this as an opt-in defaultPreset prop.

    Chart tooltips now render above the page, so a short chart (like the histogram above a list) or a clipping container no longer cuts them off.

    The Columns picker is now an icon button with a "Columns" tooltip, placed before the filters.

    <TracesExplorer.Body toolbar={toolbar} chart={<TracesExplorer.Histogram />}>
      {content}
    </TracesExplorer.Body>
  • Added time to first token (TTFT) to the Traces, Spans and Threads lists in Studio, alongside time to first chunk (TTFC). (#28898)

    Traces: TTFT is a new optional column, sortable like TTFC. Both timings can now be used as filters, on the trace itself or on any of its spans.

    Spans: TTFC and TTFT are new optional columns, sortable, and both can be used as filters.

    Threads: the list now has a column picker (messages, TTFC, TTFT and more), saved per project and in saved views. Both timings can be used as filters.

    Span details: the Timing section now shows the span's time to first chunk and time to first token when it streamed, so the root span shows the same values as the lists.

    Trace timeline: a trace opened from the Traces list shows its time to first chunk and time to first token as two dashed rows, both ending on the first chunk, under a "Metrics" row above the spans. Metrics is closed by default; open it to see the timings. You can see at a glance how much of the wait came from the model and how much from earlier steps.

  • Improved the time range filter on the traces and spans pages. (#28894)

    • Custom ranges now show the date and time, e.g. Oct 21, 2:05 PM – 4 PM, instead of the date only.
    • Clicking a single histogram bar now zooms into that bar's time span, just like dragging across bars.
    • Clearing the time range after zooming into the histogram now returns to the range you had before the zoom (for example "Last 14 days" or a custom range), instead of always jumping to "Last 7 days". Clearing again resets to the default range.
  • Added DashboardRenderer (@mastra/playground-ui/domains/dashboards), which renders a stored dashboard definition on a 12-column grid. It ships four widget kinds that rebuild today's Metrics cards from JSON: kpis, timeseries, table and scores. Each widget runs its own queries with the page's time range and filters, and a widget that fails to parse, query or render shows its own error while the rest of the dashboard still renders. (#28724)

    When the time range, a filter or the view picked from a card's title menu changes, a widget shows a skeleton until its own answer arrives instead of the previous figures, so a slow request never shows one range's numbers under another. A widget re-reading an answer it already has keeps it on screen, dimmed. KPI cards now set aria-busy while their reading loads or updates.

    The renderer reads the page's time range and filters from MetricsProvider, so render it inside one. Hosts can add their own widget kinds without changing the renderer:

    import { z } from 'zod/v4';
    import { DashboardRenderer, defineWidgetKind } from '@mastra/playground-ui/domains/dashboards';
    import { MetricsProvider } from '@mastra/playground-ui/domains/metrics';
    
    const noteKind = defineWidgetKind({
      kind: 'note',
      optionsSchema: z.object({ text: z.string() }),
      component: ({ views }) => <p>{views[0].options.text}</p>,
    });
    
    <MetricsProvider preset={preset} filterTokens={tokens} onPresetChange={setPreset} onFilterTokensChange={setTokens}>
      <DashboardRenderer
        definition={definition}
        scope="project"
        kinds={[noteKind]}
        onDrilldown={(page, scope) => open(page, scope)}
      />
    </MetricsProvider>;

    The @mastra/core peer range is now >=1.81.0-0 <2.0.0-0: the renderer imports @mastra/core/storage/dashboards, which earlier core releases don't ship.

  • Added saved views to the Traces, Spans, Threads and Logs pages. A host stores the views by wrapping the page in a SavedViewsProvider; without one the pages look as before. (#29001)

    import { SavedViewsProvider, SavedViewNameTakenError } from '@mastra/playground-ui/domains/observability/saved-views';
    
    const source = {
      list: page => api.listSavedViews(page),
      create: input => api.createSavedView(input), // throw SavedViewNameTakenError on a duplicate name
      update: (id, patch) => api.updateSavedView(id, patch),
      remove: id => api.deleteSavedView(id),
    };
    
    <SavedViewsProvider
      source={source}
      queryKeyScope={['project', projectId]}
      // Optional: views the user may only apply (no rename, delete or save changes)
      canEdit={view => view.scope !== 'org'}
    >
      <SpansPage />
    </SavedViewsProvider>;

    A searchable view picker sits at the top right of the page header, next to the Traces / Spans / Threads switch. Picking a view sets the page URL and columns exactly as setting the same filters, time range and sort by hand would; saving stores what the URL holds. Parts of an older view the page no longer supports are skipped. Traces hosts place TracesExplorer.SavedViews and logs hosts place LogsSavedViews.

    Hosts whose list pages open on a range other than the last 7 days pass it as defaultDatePreset to useObservabilityListFilters and to the saved views, so that range is left out of the URL and a view of the last 7 days reopens on the last 7 days:

    const filters = useObservabilityListFilters(urlState, { defaultDatePreset: 'last-24h' });
    <ObservabilitySavedViews page="spans" {...urlState} defaultDatePreset="last-24h" />;

    Fixed filter chips that stayed on screen after the page replaced its filters (for example when switching views), and a combobox trigger icon that rendered larger than the trigger text.

    useSavedViews now reports a failed save as error (was storageError), and SavedViewEditor takes a saveCaption for the note shown next to Save view. A saved-views controller's rename now returns whether the view was renamed (boolean or Promise<boolean>), so a rename form can stay open with the error when the name is taken.

  • Added a shared SkillSimpleForm skill editor (name, description, and Markdown instructions, with an optional read-only mode) at @mastra/playground-ui/domains/skills/skill-simple-form, so apps other than Studio can show and edit skills with the same component. (#28103)

    import { SkillSimpleForm } from '@mastra/playground-ui/domains/skills/skill-simple-form';
    
    <SkillSimpleForm
      name={name}
      onNameChange={setName}
      description={description}
      onDescriptionChange={setDescription}
      instructions={instructions}
      onInstructionsChange={setInstructions}
    />;
  • Hover highlights no longer glide between items. DropdownMenu, ContextMenu, Select, Combobox, Command, CommandPalette, FilterBar, PropertyFilter, Pagination and DataList rows each paint their own hover and keyboard highlight, and switch instantly. The current page in Pagination keeps its raised pill without sliding. (#28757)

    Removed

    • @mastra/playground-ui/hooks/use-fluid-hover (useFluidHover, useRegisterFluidHoverItem) and @mastra/playground-ui/components/fluid-hover-highlight (FluidHoverHighlight). Give each row its own hover style instead.
    • The highlightClassName prop of CommandList. Style the row radius on CommandItem instead.
    // Before
    const hover = useFluidHover(listRef);
    <div ref={listRef} className="relative" {...hover.handlers}>
      <FluidHoverHighlight hover={hover} className="rounded-lg bg-fill" />
      {items.map((item, i) => (
        <Row key={item.id} ref={el => hover.registerItem(i, el)} />
      ))}
    </div>
    
    // After
    <div>
      {items.map(item => (
        <Row key={item.id} className="rounded-lg hover:bg-fill" />
      ))}
    </div>
    // Before
    <CommandList highlightClassName="rounded-lg">{items}</CommandList>
    // After
    <CommandList>{items}</CommandList>
  • Added METRICS_DASHBOARD, the built-in Metrics dashboard as a dashboard definition. Pass it to DashboardRenderer to render the Metrics page: token usage and agent runs (with its failure rate view), latency and distinct IDs, then the breakdown, on org and project pages alike. The definition is frozen; copy it with structuredClone before customising. (#28734)

    import { DashboardRenderer, METRICS_DASHBOARD } from '@mastra/playground-ui/domains/dashboards';
    
    <DashboardRenderer definition={METRICS_DASHBOARD} scope={scope} onDrilldown={openDrilldown} />;
  • Cleaned up the tables in metrics cards: one table for figures, and a share bar that is only a chart. (#28762)

    Replaced MetricsShareList with ShareBar

    ShareBar shows each row's share of a total: the bar, then the column headers, then a compact legend row per segment. Sorting, the no-bar mode and header placement options are gone; use MetricsTable for a sortable table of figures.

    // Before
    <MetricsShareList rows={rows} columns={columns} valueLabel="Spend" headerPlacement="below-strip" />
    
    // After
    <ShareBar rows={rows} columns={columns} valueLabel="Spend" />

    MetricsTable is ready for full tables

    • pageSize adds pagination, and isLoading shows skeleton rows under the real header instead of the empty message.
    • A column's description explains it in a tooltip on the header text, marked by a dotted underline.
    • A third click on a sorted column turns sorting off, back to defaultSort or the given order.
    • isRowActive highlights a row, for example the one a page is filtered to.
    • Headers now line up with their columns, figure columns share spare width with the card, and every table uses the same 34.5px rows.
    <MetricsTable
      aria-label="Agent runs"
      columns={[
        { id: 'name', header: 'Agent', width: 'minmax(12rem,3fr)', cell: row => row.name },
        {
          id: 'runs',
          header: 'Runs',
          align: 'end',
          description: 'Runs that started in this range.',
          sortValue: row => row.runs,
          cell: row => formatCount(row.runs),
        },
      ]}
      rows={rows}
      getRowKey={row => row.name}
      defaultSort={{ column: 'runs', direction: 'desc' }}
      pageSize={13}
      isLoading={isLoading}
      empty="No agent runs in this range."
    />

    New pieces for table rows

    • SegmentedName shows a name in parts, such as openai › gpt-5. The traces timeline uses it for span names.
    • SeriesMark takes an optional color: without one it is a neutral tile, for tables without a chart. ProviderMark puts a provider logo in it.

    The metrics Breakdown card now uses MetricsTable, with an icon before each agent, workflow, tool or model name, and each model's provider.

  • Added a chip to tool-call cards for calls that continue as MCP tasks. It shows the task's status and elapsed time, with a button to copy the MCP task id. The chip appears for MCP servers the client accepts tasks from: (#28629)

    const mcpClient = new MCPClient({
      servers: {
        reports: { url: new URL('https://reports.example.com/mcp'), tasks: {} },
      },
    });

Patch Changes

  • Added an inline appearance to Activity and a flush option to ActivityContent, for activity lines that sit inside prose. (#28709)

  • Added ActivityStretchLine under @mastra/playground-ui/domains/chat/activity. (#28709)

  • Improved the span details panel in Studio traces so each span says what it did before showing its raw payloads. (#28687)

  • Improved metrics chart resizing. (#28700)

  • Fixed markdown tables, lists and paragraphs losing their cell padding, borders, bullets and spacing when a notice's stylesheet loaded before the app's Tailwind entry. (#28834)

  • Redesigned the Studio span details panel so the most useful information is easier to find, especially in narrow columns. (#28856)

  • Shortened the pagination "Previous" label to "Prev" so it matches the length of "Next". (#28825)

  • Chart tooltips on Metrics, Traces and Spans no longer change width as you move between points. (#28833)

  • Improved hover on metrics charts (MetricsLineChart, MetricsStackedBarChart): (#28701)

  • Message footers read at caption size. (#28834)

  • Removed unused internal chart code while preserving the public component API. (#28737)

  • Added autoScrollStartFollowing and onAutoScrollFollowingChange to ScrollArea. (#28709)

  • Added Model and Provider filters to the Spans page. (#28994)

  • Fixed focus rings showing after mouse clicks. (#28694)

  • Security: bump zod to ^4.6.7 (fixes advisory affecting zod <4.6.6). (#28983)

  • Improved the trace span views in Studio: (#28867)

@mastra/react@1.14.0

Patch Changes

  • Added threads as a column preferences scope, so the Threads list can save its own column selection. (#28898)

  • Experiment details now load instantly from cache when navigating back to an experiment, instead of showing a loading state while refetching. (#28813)

  • useTraceQuery keeps new traces in order when the list is sorted by timeToFirstTokenMs. (#28699)

@mastra/redis-streams@0.8.0

Minor Changes

  • Idle topic streams now expire after 7 days by default. Previously streamIdleTtlMs defaulted to 0, so a pubsub created without it never expired any stream. A production deployment built that way accumulated thousands of never-expiring conversation streams until Redis ran out of memory. (#28968)

    Every write still refreshes the TTL, so a topic in active use never expires. If a grouped worker can be offline for more than 7 days while publishers are quiet, and must replay the backlog when it comes back, raise the TTL or turn expiry off:

    // Before: streams created by this pubsub never expired
    // After: they expire after 7 days without a write
    new RedisStreamsPubSub({ url });
    
    // To keep the old behavior, turn expiry off explicitly
    new RedisStreamsPubSub({ url, streamIdleTtlMs: 0 });

    Check your eviction policy before upgrading. Under a volatile-* policy, Redis only evicts keys that have a TTL. Streams used to have none, so they were safe from eviction; now they can be evicted under memory pressure, losing events. Set maxmemory-policy to noeviction.

    Existing streams created without a TTL only get one on their next write. See the reference docs for a runbook to reclaim them.

    The pubsub also logs a warning once after connecting if the server has maxmemory set with any policy other than noeviction. Evicting policies (Amazon ElastiCache defaults to volatile-lru) delete live streams, consumer groups, and lease keys under memory pressure.

    Also in: @mastra/valkey-streams@0.6.0

Patch Changes

  • Fixed subscribe() hanging forever when Redis accepts the connection but never answers, for example a frozen server or a proxy that has stopped forwarding. (#28941)

@mastra/server@1.81.0

Minor Changes

  • Added spanQueryOutcomeFields and traceQueryOutcomeFields to the observability capabilities response. Span and trace query, aggregate, and discovery routes return 501 when a request uses outcome or stopReason against a store that does not support them. (#28884)

    const { capabilities } = await client.getObservabilityCapabilities();
    if (capabilities.spanQueryOutcomeFields) {
      // outcome and stopReason predicates, groupBy, and countDistinct are available on span routes
    }
  • Added Studio routes for an MCP server's tasks: (#28629)

    • GET /mcp/:serverId/tasks lists tasks newest first, 50 per page by default and at most 200. Filter by status, surface or client, and pass nextCursor for older tasks.
    • POST /mcp/:serverId/tasks/find finds one task by its MCP task id, sent in the body.
    • POST /mcp/:serverId/tasks/:taskKey/cancel cancels a task.
    • POST /mcp/:serverId/tasks/:taskKey/input answers a task's input requests.

    Listing and finding need mcp:read. Cancelling and answering need mcp:write. Server list and detail responses include tasks_capability when a server runs tools as tasks. Sessions scoped to a resource get no task list, since MCP clients and Mastra resources are separate identities.

    const res = await fetch('/api/mcp/reports/tasks?status=input_required');
    const { tasks, nextCursor } = await res.json();
    if (tasks[0]) await fetch(`/api/mcp/reports/tasks/${tasks[0].taskKey}/cancel`, { method: 'POST' });
  • The span aggregate route accepts model and provider in groupBy and countDistinct when the store supports them. getObservabilityCapabilities() reports support as spanAggregateModelDimensions. Against an older store, these requests return 501 before storage runs. (#28908)

    const { capabilities } = await mastraClient.getObservabilityCapabilities();
    
    if (capabilities.spanAggregateModelDimensions) {
      const { rows } = await mastraClient.aggregateSpans({
        timeRange,
        groupBy: ['rootEntityName'],
        measures: ['count', 'countDistinct.model'],
      });
    }
  • Span query and span aggregate routes accept model and provider filters when the store supports them. getObservabilityCapabilities() reports support as spanQueryModelFields. Against an older store, these filters return 501 before storage runs. (#28859)

    const { capabilities } = await mastraClient.getObservabilityCapabilities();
    
    if (capabilities.spanAggregate && capabilities.spanQueryModelFields) {
      const { rows } = await mastraClient.aggregateSpans({
        timeRange,
        where: { op: 'eq', left: { path: 'provider' }, right: { literal: 'anthropic' } },
        groupBy: ['name'],
        measures: ['count'],
      });
    }
  • Trace, thread, span and aggregate queries now accept timeToFirstTokenMs predicates, and trace and span queries can sort by it. The new traceQueryTimeToFirstToken and traceQueryTimeToFirstTokenSort capabilities tell you whether the configured observability store supports them. When it doesn't, the request returns 501 with a clear message before the store runs. (#28699)

Patch Changes

  • Agent generate and stream routes now respond with HTTP 400 instead of 500 when the request context fails the agent's requestContextSchema. (#28918)

  • Fixed GET /api/workspaces and GET /api/workspaces/:workspaceId reporting hasFilesystem: false and hasSandbox: false for workspaces whose filesystem or sandbox is a resolver function. (#29027)

  • Preserve zero-length message context ranges in memory search and use the default range when messageRange is omitted. (#28769)

@mastra/spanner@1.11.0

Minor Changes

  • Added storage for the optional background task fields expiresAt and metadata, plus filtering by expiresAt. Existing tables gain the new columns automatically when the store initializes. (#28425)

    Fixed background task updates failing with this.database.runUpdate is not a function, which stopped background tasks from running, suspending, or completing on Spanner.

@mastra/stagehand@0.4.0

Minor Changes

  • Added setCookies() support to StagehandBrowser. Cookies set before a thread's browser opens are added before it loads any page, so pages open signed in. (#28631)

    await browser.setCookies([{ name: 'my_app_session', value: token, domain: 'app.example.com', path: '/' }], threadId);

Patch Changes

  • Fixed the Stagehand browser not noticing when its Chrome is killed outside Mastra (for example with kill or a crash). (#28794)

@mastra/vercel@1.6.3

Patch Changes

  • Fixed the default sandbox instructions to distinguish Mastra's unavailable Workspace mounting tools from Vercel Sandbox's native FUSE support. (#28729)

Other updated packages

Dependency updates only: @mastra/arize@1.3.28, @mastra/arthur@0.4.28, @mastra/auth-better-auth@1.1.10, @mastra/datadog@1.4.20, @mastra/deepeval@0.1.22, @mastra/editor@0.16.6, @mastra/express@1.5.25, @mastra/fastify@1.5.25, @mastra/google-cloud-pubsub@1.1.6, @mastra/hono@1.7.23, @mastra/laminar@1.3.30, @mastra/langfuse@1.6.4, @mastra/mcp-docs-server@1.3.11, @mastra/nestjs@0.2.40, @mastra/next@0.2.39, @mastra/opencode@0.1.40, @mastra/otel-bridge@1.5.20, @mastra/posthog@1.4.5, @mastra/sentry@1.2.30, @mastra/tanstack-start@0.2.39, @mastra/turso@0.1.19