Skip to content

August 19, 2026

Latest

Choose a tag to compare

@PaulieScanlon PaulieScanlon released this 19 Aug 15:45
· 18 commits to main since this release

Highlights

Durable execution for Agents API (no deploy required)

Stored agents created via the Agents API can now run with durable: true, enabling durable execution (with configurable loop settings) without shipping new code, and inheriting cache/pubsub from your server for multi-replica durability.

Cloudflare Sandbox provider for remote workspaces

The new @mastra/cloudflare-sandbox provider runs commands and file operations via a deployed Sandbox Bridge Worker, adding a new deployment/runtime option for Mastra workspaces.

MCP protocol upgrades: stateless 2026-07-28 + multi-round elicitation

@mastra/mcp adds opt-in support for the stateless 2026-07-28 protocol revision (server + client) plus updated elicitation support using the spec’s multi round-trip mechanism, enabling more modern MCP interoperability without breaking legacy connections.

Sandbox checkpoints (Local + Platform/Railway capability signaling)

Sandboxes now advertise supportsCheckpoints, and LocalSandbox gains filesystem-backed checkpoints (checkpointName/seedCheckpointName) so sessions can boot from and persist real snapshots—unlocking faster warm-start patterns (also supported in Platform and Railway sandboxes).

RAG: persistable GraphRAG snapshots

@mastra/rag adds serialize() and GraphRAG.deserialize() so knowledge graphs can be saved/restored instead of rebuilt on every start, dramatically reducing startup cost for large corpora.

Breaking Changes

  • None called out in this changelog.

Changelog

@mastra/core@1.60.0

Minor Changes

  • Added ProcessHandle.closeStdin() to signal end-of-file to background processes. Local and Docker sandboxes support closing stdin, while providers without an available stdin-close API return a provider-specific unsupported-operation error. Providers signal the unsupported case with the new UnsupportedStdinCloseError, and the base class supplies that behavior by default so existing ProcessHandle subclasses keep compiling. Calling handle.writer.end() also closes stdin, and finishes without an error when the provider cannot close stdin. (#21606)

    Also in: @mastra/agentcore@0.4.0, @mastra/apple-container@0.4.0, @mastra/blaxel@0.7.0, @mastra/daytona@0.8.0, @mastra/docker@0.6.0, @mastra/e2b@0.9.0, @mastra/modal@0.5.0, @mastra/platform-workspace@1.3.0, @mastra/railway@0.6.0, @mastra/vercel@1.4.0

  • Added live-tail PubSub subscriptions with startFrom: "latest" and an explicit supportsOffsets capability. (#21535)

    await pubsub.subscribe(topic, callback, { startFrom: 'latest' });

    The default remains "earliest", and existing consumer groups keep their checkpoint.

  • Added optional filename property to FilePayload. When an agent emits a file chunk with a filename, the Channel layer now preserves it through to the Chat SDK adapter instead of generating a name from the MIME type. Existing file chunks without a filename continue using the generated.<ext> fallback. (#21135)

    const chunk: FilePayload = {
      data: fileBuffer,
      mimeType: 'text/plain',
      filename: 'report.txt', // preserved through to the channel adapter
    };
  • Client-executed tools (tools without a server-side execute) now fire their onOutput lifecycle hook when the browser returns the tool result on a follow-up request. Previously onOutput only fired after a server-side execute, so client tools never reported their output. (#21377)

    const agent = new Agent({
      // ...
      tools: {
        browserTool: createTool({
          id: 'browserTool',
          description: 'Runs in the browser',
          inputSchema: z.object({ query: z.string() }),
          // No execute — the client runs the tool. This hook now fires when
          // the client sends the result back.
          onOutput: async ({ toolCallId, output }) => {
            console.log('client tool resolved', toolCallId, output);
          },
        }),
      },
    });

    The hook fires for trailing tool results that match a raw tool call from the preceding assistant message, so the follow-up request must include both (as @mastra/client-js does automatically). This works with standard, legacy, and durable agents, including requests with a same-name serialized clientTools entry. The callback receives { toolCallId, toolName, output, abortSignal }. Because the input is client-provided, treat the output as untrusted data. Delivery is at least once, and the hook does not fire when an input processor rejects the request. Keep hooks idempotent.

  • Added AgentControllerWireEvent, JsonReadyAgentControllerEvent, ErrorCarryingAgentControllerEvent and WireDisplayState to @mastra/core/agent-controller. They describe an agent controller event as it crosses an HTTP boundary — display-state Maps as records, errors as { name, message }, dates as ISO strings — and are derived from AgentControllerEvent itself, so a client can type what it receives without redeclaring the controller's events by hand. (#21761)

    import type { AgentControllerWireEvent } from '@mastra/core/agent-controller';
    
    function onEvent(event: AgentControllerWireEvent) {
      if (event.type === 'display_state_changed') {
        // a record after JSON, where the controller holds a Map
        Object.values(event.displayState.activeTools);
      }
      if (event.type === 'error') {
        event.error.message;
      }
    }
  • MCP tools served over HTTP now see the authenticated caller. When an MCP server runs behind a Mastra server with server.auth configured, the resolved user is bridged into extra.authInfo automatically, on both the streamable HTTP and SSE transports. Previously extra.authInfo was always undefined because the request handed to the MCP transport was rebuilt without the auth data. (#21689)

    Custom verification

    If your own middleware verifies the caller, build the auth info yourself with the new server.mcpOptions.setRequestAuth hook:

    export const mastra = new Mastra({
      mcpServers: { myServer },
      server: {
        middleware: [verifyBearerToken],
        mcpOptions: {
          setRequestAuth: (req, requestContext) => {
            const payload = requestContext.get('bearerPayload');
            req.auth = { token: payload.token, clientId: payload.sub, scopes: payload.scope.split(' ') };
          },
        },
      },
    });

    Fixes #17291

    Also in: @mastra/hono@1.7.0, @mastra/mcp@1.17.0, @mastra/server@1.60.0

  • Added an opt-in propagate flag on the FGA actor signal. Set actor: { actorKind: 'system', propagate: true } when starting a workflow run and the actor is forwarded into the agent and tool calls made for declarative .then(agent) and .then(tool) steps, so system and scheduled runs no longer fail membership resolution. Propagation stays opt-in, skips custom step execute functions, and can be overridden per step. (#19064) (#21713)

  • Added per-message Signal metadata to Channel handlers. Use ctx.signalMetadata to attach serializable, non-sensitive context that follows both idle and active message delivery: (#21330)

    handlers: {
      onDirectMessage: async (thread, message, defaultHandler, ctx) => {
        ctx.signalMetadata.attachmentIds = ['file-1']
        await defaultHandler(thread, message)
      },
    }
  • Added delegation.hookErrorStrategy so a failing delegation hook no longer passes silently. (#21695)

    Previously, if onDelegationStart, messageFilter, or onDelegationComplete threw, the error was only logged and the delegation carried on. A throwing onDelegationStart could not block a subagent, and a throwing onDelegationComplete lost whatever the hook was responsible for with no programmatic signal.

    Detecting hook failures

    Hook failures are now always recorded on the run's request context, whatever strategy you choose:

    const requestContext = new RequestContext();
    await parentAgent.generate('Research AI trends', { requestContext });
    const hookErrors = requestContext.get('__mastra_delegationHookErrors') ?? [];

    Failing the delegation instead

    Opt in to fail-closed behavior. A throwing onDelegationStart then blocks the subagent, and a throwing messageFilter or onDelegationComplete surfaces to the parent as a failed tool call:

    await parentAgent.generate('Research AI trends', {
      delegation: { hookErrorStrategy: 'throw', onDelegationComplete },
    });

    The default remains 'warn', so existing behavior is unchanged. Fixes #21624.

  • Added checkpoint support to LocalSandbox and a checkpoint capability signal to sandboxes. Sandboxes now expose supportsCheckpoints so features can detect whether snapshot() persists real state. LocalSandbox gained filesystem-backed checkpoints: pass checkpointName to seed the working directory on start() and persist it on snapshot(), and seedCheckpointName as a boot-only fallback (for example a shared warm base image) that never gets overwritten by later snapshots. (#21798)

    import { LocalSandbox } from '@mastra/core/workspace';
    
    const sandbox = new LocalSandbox({
      workingDirectory: './workspace',
      checkpointName: 'session-123',
      seedCheckpointName: 'repo-base',
    });
    
    await sandbox.start();
    await sandbox.snapshot();
  • Added modelSettings.timeout so you can put a time limit on agent runs. Set totalMs to cap how long a whole run may take, including every reasoning step, tool call and retry, and stepMs to cap a single model call. Going over either limit fails with a MastraTimeoutError. A totalMs timeout ends the run outright, while a stepMs timeout moves on to the next model when you have configured fallback models, so a slow provider no longer stalls the run. Closes #15667 (#21724)

  • Added foundational support for an upcoming experimental memory capability across storage, runtime, and developer tooling. (#19538)

    Also in: @mastra/code-sdk@1.3.0, @mastra/factory@0.8.0, @mastra/libsql@1.21.0, @mastra/memory@1.27.0, @mastra/mongodb@1.18.0, @mastra/mysql@0.8.0, @mastra/pg@1.21.0

  • Added a durable option to stored agents so agents created through the Agents API can run with durable execution — no code deployment required. (#21715)

    await mastraClient.createStoredAgent({
      id: 'helper',
      name: 'Helper',
      instructions: 'You are a helpful assistant.',
      model: { provider: 'openai', name: 'gpt-5' },
      durable: true,
    });

    Pass true for defaults, or { maxSteps, cleanupTimeoutMs } to tune the durable loop. Cache and pubsub are inherited from the server's Mastra instance, so configure distributed backends there for durability across replicas. Automatic recovery is still configured in code via recovery.durableAgents.

    Also in: @mastra/client-js@1.41.0, @mastra/dsql@1.3.1, @mastra/editor@0.14.0, @mastra/libsql@1.21.0, @mastra/mongodb@1.18.0, @mastra/mssql@1.7.1, @mastra/mysql@0.8.0, @mastra/pg@1.21.0, @mastra/server@1.60.0, @mastra/spanner@1.6.1

  • Added an opt-in persistPartialOnAbort stream option that saves the assistant text streamed before a cancellation. Only the pre-abort snapshot is persisted, so output a provider keeps producing after cancellation is discarded, and nothing is saved when no text was streamed. Aborted streams still persist nothing by default. Also fixed thread creation tracking so a thread created during step persistence is no longer re-created on finish. Fixes #17510. (#21600)

  • Added invoker-bound tool provider connections. Providers now receive the connection kind, toolkit, and live RequestContext so they can execute as the authenticated user without coupling provider identity to the Memory resource. The stored connection ID continues to select the exact provider account. (#21783)

    import type { ToolProviders } from '@mastra/core/tool-provider';
    
    const toolProviders: ToolProviders = {
      crm: {
        tools: {
          CREATE_LEAD: { toolkit: 'salesforce' },
        },
        connections: {
          salesforce: [
            {
              kind: 'invoker',
              toolkit: 'salesforce',
              connectionId: 'connected-account-id',
            },
          ],
        },
      },
    };

Patch Changes

  • Update provider registry and model documentation with latest models and providers (587f6ef)

  • Fixed workspace skill resolvers so concurrent requests keep their own resolved skill set throughout execution. (#21524)

  • Fixed routed models so tool results can include images and files. (#21183) (#21569)

  • Fixed model-backed processors (language detector, prompt injection detector, PII detector, system prompt scrubber, and moderation) dropping the request context. Their internal detection agents now receive the caller's RequestContext, so dynamic model resolvers and gateways can select models per request. (#21709)

  • Fixed the tail parameter of the workspace execute_command and get_process_output tools rejecting numeric strings. Models sometimes send number parameters as strings, for example "10" instead of 10. The timeout parameter already tolerated this, but tail failed validation and the command never ran, wasting a turn. In observed cases the model then fabricated a success result instead of retrying. Numeric strings are now coerced to numbers for tail in both tools. (#21493)

  • Fixed generated streams to preserve provider metadata on text and file events. (#21441)

  • Added settled() to the base memory class. Memory implementations can do work in the background after an agent run returns, and this gives callers a way to wait for it before closing a storage connection they own. The default implementation does nothing; @mastra/memory overrides it. (#21708)

    Also in: @mastra/memory@1.27.0

  • Fixed workflow cancellation so sleep() and sleepUntil() stop promptly without overwriting the canceled run status. (#21570)

  • Fixed evented workflow restarts so conditional branches resume at the correct execution path and aggregate their results. (#21603)

  • Improved observational memory progress restoration by rebuilding UI status from the durable memory record instead of polluted message history. Restored progress now also reports the memory thresholds you configured, where it previously always showed the built-in defaults. (#21604)

  • Refactor MastraCompositeStore domain wiring to be data driven. The constructor resolve block and the init roll call previously enumerated every storage domain by hand, so a domain registered in the stores map but missing from either list was silently skipped: init reported success while the domain's tables were never created. Both lists are now replaced by iteration over a typed DOMAIN_KEYS constant with a compile time exhaustiveness guard, and a conformance test proves a newly registered domain cannot dodge init. DOMAIN_KEYS is a new exported constant, following the existing EDITOR_DOMAINS pattern. No behavior change for the existing domains, with two narrow carve outs: a stores map entry without an init function is now skipped during init instead of throwing a TypeError, and an entry present in the stores map that the old roll call never named now gets initialized. (#21672)

  • Fixed agent memory so that calls now fail immediately when the given thread belongs to a different resource. Previously agent.stream() and agent.generate() would silently run the model and drop the turn instead of reporting the ownership mismatch (#21641). (#21691)

  • Fixed durable agents pausing between model steps by suppressing unused internal workflow step events that repeatedly serialized cumulative conversation state. (#21529)

  • Fixed durable step-start events publishing full model requests. (#21572)

  • Fixed replay streams so disconnecting during history replay also stops the live stream. (#21556)

  • Fixed dataset experiments with memory-enabled agents when no resource or thread IDs are provided. (#21552)

  • Fixed tool request context schemas to pass transformed values, coercions, and defaults to execute while retaining input-form values for nested validation. Explicit mutations now preserve schema input encoding and reject unencodable transformed writes without corrupting the shared context. (#21653)

  • Skills discovery no longer blocks agent turns: the skills processors serve the cached catalog and revalidate in the background, and refresh swaps the catalog atomically. Mid-session skill changes now appear one turn later (plus a staleness cooldown of up to 30 seconds); pass blockingRefresh: true to SkillsProcessor or SkillSearchProcessor to restore same-turn freshness by awaiting the refresh before the first step. (#21555)

  • Restored the Studio memory bar token counts after a reload by reading them from the stored observational memory record. (#21604)

    Also in: @mastra/memory@1.27.0, @mastra/playground-ui@50.0.0

  • Retry transient Windows file locks when atomically replacing provider registry cache files. (#21425)

  • Fixed DurableAgent.listActiveRuns() and recoverActiveRuns() loading every running run's full workflow snapshot into memory at once. Candidate runs are now fetched from storage in bounded batches of 100 rows, so discovering or recovering runs against a large backlog no longer risks exhausting process memory. Fixes #21501. (#21518)

  • Fixed array textStream output so it always stays valid JSON, even when the first streamed chunk already includes elements (#21559)

  • Exported ReservedThreadMetadataKey, the list of thread-metadata keys an agent controller session owns for its own bookkeeping (selected model and mode, observer/reflector config, token usage, persisted preferences). Packages that cannot import the list as a value can now pin their copy of it to the real one: (#21739)

    import type { ReservedThreadMetadataKey } from '@mastra/core/agent-controller';
    
    const RESERVED = { currentModelId: true /* … */ } satisfies Record<ReservedThreadMetadataKey, true>;

    The list itself is unchanged — only its keys are now nameable, so a package that mirrors it fails to compile the moment the two fall out of step.

  • Added regression coverage keeping memory-sourced messages exempt from the thread ID check, so resource-scoped observational memory can pull in messages from a resource's other threads without failing the turn. (#21702)

  • Honor stored agent version overrides on direct programmatic agent calls. Version overrides supplied via MASTRA_VERSIONS_KEY on the requestContext, the versions call option, or Mastra-level versions defaults now resolve the called agent itself to its stored version (previously they only applied to sub-agent delegation and HTTP routes), fixing the inconsistency where published overrides were served over HTTP but silently ignored in programmatic paths. (#21383)

  • LocalSandbox now replaces an existing checkpoint atomically. A concurrent boot never observes a missing or half-written checkpoint while a snapshot is being saved. (#21798)

  • Fixed tool search and skill search losing loaded state for HTTP requests that pass a thread via memory options. Processors now resolve the thread ID from the memory request context, and single-name load_tool results are recognized when reconstructing loaded tools from conversation messages with storage: 'context'. Fixes #21508 (#21521)

  • Fixed durable run recovery so a reconnecting thread subscriber receives the remaining output and terminal event of a recovered run. (#21526)

    Only one recovery of a run can be active at a time. A second concurrent durableAgent.recover(runId) call now fails fast with the error DURABLE_AGENT_RECOVER_ALREADY_IN_PROGRESS instead of restarting the run twice.

  • Fixed durable agent runs failing during shutdown by waiting for in-flight workflow persistence before closing storage. (#21532)

  • Rewrite system-reminder tags in Azure-bound user text and system instructions to avoid content moderation refusals while preserving stored history. (#21599)

  • Fixed MockMemory so its working memory merge keeps parity with @mastra/memory: a null now deletes a field on the first write and inside newly created nested objects, instead of being stored literally. (#21687)

    Also in: @mastra/memory@1.27.0

  • Fixed stale tool suspension metadata when resuming with agent.resumeStream(). (#21319)

    A tool suspended with suspendSchema left its suspendedTools entry on the saved assistant message after it was resumed via agent.resumeStream(resumeData, { runId, toolCallId }). A client reloading the thread read the already-resolved tool as still waiting for input, and could send the next resume to the wrong tool call. The entry is now cleared for both resume conventions.

    Falsy resume payloads are handled correctly

    A tool whose resumeSchema is a primitive can legitimately be resumed with false, 0 or "", which is how a boolean human-in-the-loop tool declines. Those payloads now clear the suspension entry, and a delegated agent-* or workflow-* tool resumed with one keeps resuming its existing sub-run instead of silently starting a fresh one.

    Approving one tool call no longer clears another's pending state

    When two calls of the same tool were waiting and one of them was approved after its approval requirement had changed, the other call's pending state could be removed, leaving it stuck and unresumable. Approving one call now leaves the other waiting as expected.

    Fixes #19083.

  • Sub-agent delegation no longer attempts to resume when the model supplies resumeData without a suspended run. Previously the delegation step chose the resume path on resumeData alone and called resumeGenerate/resumeStream with an undefined run id, throwing AGENT_RESUME_NO_SNAPSHOT_FOUND before the sub-agent executed. It now starts a fresh delegation in that case. (#21729)

  • Stop writing run-local workflow events to the server cache. (#21675)

    Durable agents wrap mastra.pubsub in a CachingPubSub, so every publish was mirrored into the shared cache for replay. Per-run workflow.events.v2.<runId> watch events are only ever consumed in the publishing process, and their payloads accumulate step results — mirroring them filled shared stores (e.g. Redis) with lists no other instance could read.

    CachingPubSub already skipped caching for publishes marked localOnly, but the caching layer sits above the proxy that sets that flag, so it never saw it. CachingPubSubOptions now accepts an optional shouldCache?: (topic: string) => boolean predicate, and the durable agent uses it to keep run-local topics out of the cache. Those events are still delivered live to subscribers; agent stream topics are unaffected and still replay as before.

  • Estimate media token cost in TokenLimiterProcessor instead of serializing base64 payloads as text. File parts and media-shaped tool results ({ data, mediaType }) previously fell through to JSON.stringify, so a single image could add thousands of phantom tokens and truncate history unnecessarily (#21731). (#21737)

  • Fixed getWorkflowRunById and getWorkflowRunSteps so nested workflows report the correct single suspended leaf step. Obsolete suspension details are removed after the step resumes or completes. (#21229) (#21565)

  • Fix per-step reasoningText and reasoning accumulating across steps for reasoning models. Each step in a multi-step run now reports only the reasoning produced during that step, matching the existing behavior of the per-step text field. Run-level reasoningText and reasoning remain the full concatenation across all steps. (#21711)

  • Fixed createStep(tool) sometimes misinterpreting a tool as a custom step. Copied or renamed tools — such as tools resolved through a tool provider — are now correctly detected and build a proper tool step. This applies to both the default createStep and the one exported from @mastra/core/workflows/evented. (#21512)

  • Restore pending tool approval state when reloading v6 UI messages. (#17948)

  • Fix AgentController subscriptions hanging when remote runs lose their lease (#21540)

  • Fixed output processors missing response messages after a mid-run memory save (#21204). (#21533)

  • Preserve channel approval cards when tool output is hidden or suppressed by a custom renderer. (#21402)

    Resolves #21162

    Custom renderers can reuse the built-in approval and tool-event formatting:

    import { formatToolApproval, renderBuiltInToolEvent, type ToolDisplayFn } from '@mastra/core/channels';
    
    const renderTool: ToolDisplayFn = event =>
      event.kind === 'approval'
        ? formatToolApproval(event.displayName, event.argsSummary, event.toolCallId, true)
        : renderBuiltInToolEvent(event, 'cards');
  • Stop evicting long-running agent runs at the suspended-run TTL. MASTRA_SUSPENDED_RUN_TTL_MS now bounds how long a run-scoped internal workflow may sit idle, not how long a run may take. Previously the lazy sweep measured wall-clock age from registration, so any run that legitimately executed past the TTL (30 minutes by default) had its workflow registration and run scope dropped mid-flight and went silent. Abandoned or suspended-and-never-resumed runs are still released on the same bound, so the memory protection is unchanged. Operators who raised the knob to work around this can return it to the default. (#21693)

  • onAbort now receives the partial assistant text streamed before a mid-generation abort, alongside the completed steps (#21718)

  • Added public read-only thread query methods on AgentController and a initStorage() method that initializes storage without provisioning a workspace. Use these to read threads or messages without paying the workspace/sandbox startup cost that createSession incurs. (#21474)

    // Before: had to create a session (which called Workspace.init() -> sandbox.start())
    const session = await controller.createSession({ resourceId });
    const threads = await session.thread.list();
    
    // After: read directly from storage, no session, no workspace
    const threads = await controller.queryThreads({ resourceId });
    const messages = await controller.queryThreadMessages({ threadId, limit: 50 });
    const thread = await controller.queryThreadById({ threadId });

    queryThreads, queryThreadById, and queryThreadMessages were already used internally; they are now part of the public AgentController API. Each lazily calls initStorage(), so callers don't need to pre-init.

  • Added stable submit_plan identity to suspension payloads and resumed results. (#21658)

  • Fixed tool results being lost when an agent calls a server-side tool and a client-side tool in the same step. The server-side tool's result is now streamed to the client and saved to history before the agent hands control back for the pending client-side tool, so clients no longer wait forever on a tool call that already finished. (#21688)

  • Fixed model configuration validation so configs with non-string routing fields (id, providerId, or modelId) are rejected with the standard "Invalid model configuration provided" error instead of failing later inside the model router. Fixes #21588 (#21664)

  • Require workflow execution authorization when resuming runs through core APIs (#21602)

  • Fixed ToolCallFilter destroying stored tool calls. The filter used to rewrite the shared message list, so its filtered history was written back to memory and the original tool calls and results were permanently replaced with text. It now filters only the prompt sent to the model, leaving stored messages, memory, and UI history untouched. (#21631) (#21669)

    Behavior change: tool calls made during the current agent loop are no longer filtered by default, so the agent can still act on results it just produced. Use filterAfterToolSteps to filter during the loop:

    // Keeps only the two most recent tool-producing steps
    new ToolCallFilter({ filterAfterToolSteps: 2 });
  • Fixed automatic tool resumption so approval-gated tools require an explicit trusted approval decision and remain suspended for ambiguous input. (#21539)

  • Fixed requestContextSchema typing so it matches the open-map runtime without losing declared-key safety, and restored assignability of schema-typed agents to bare Agent. (#21525)

    Declared keys on get/set/has/delete stay strictly typed (typos still fail). Runtime-only keys — including reserved middleware keys like mastra__resourceId — are available through getRaw/setRaw/hasRaw/deleteRaw, which return/accept unknown instead of forcing as never casts.

    Also fixed generic helpers typed as (agent: Agent) => ... rejecting agents that declare a requestContextSchema (TRequestContext now defaults to any on Agent/SubAgent so the invariant generic remains assignable).

    const ctx = new RequestContext<{ tenantTier?: 'free' | 'pro' }>();
    ctx.set('tenantTier', 'pro');
    ctx.setRaw('session.cache', { hits: 0 });
    const cache = ctx.getRaw('session.cache'); // unknown
    
    const schemaAgent = new Agent({
      id: 'repro',
      name: 'repro',
      instructions: 'hi',
      model: 'openai/gpt-5.6-sol',
      requestContextSchema: z.object({ tenantTier: z.enum(['free', 'pro']).optional() }),
    });
    declare function driveAgent(agent: Agent): Promise<void>;
    void driveAgent(schemaAgent); // ok

    Fixes #21286

  • Fixed Anthropic cache-write usage to preserve 5-minute and 1-hour token counts across streams and observability. (#21563)

  • Added a blocking option to AgentController.onSessionCreated. Blocking listeners are awaited before createSession() resolves, so hosts can seed session state (for example observational-memory settings loaded from storage) before the caller can start a run. Blocking listeners run sequentially in registration order before fire-and-forget listeners are notified. Listener failures remain isolated and logged; default (non-blocking) listeners keep their fire-and-forget behavior. (#21423)

    controller.onSessionCreated(
      async session => {
        // Runs before createSession() resolves for a newly materialized session.
        const settings = await loadSettings(session.identity.getResourceId());
        if (settings) await session.state.set(settings);
      },
      { blocking: true },
    );
    
    // Default listeners stay fire-and-forget.
    controller.onSessionCreated(session => audit(session));
  • Fixed workflow snapshot persistence emitting duplicate durable operation IDs. Running a workflow on a durable engine such as @mastra/inngest logged AUTOMATIC_PARALLEL_INDEXING warnings whenever a step suspended, because two snapshot writes on the same execution path shared one operation ID. Each snapshot write now uses a distinct ID, so suspend, resume, cancel, pause and sleep runs no longer produce the warning. Fixes #21639. (#21696)

  • Fixed DurableAgent snapshots to prune duplicated foreach suspension data so long-running agents do not exceed storage document limits. (#21537)

  • Fix sub-agent delegation permanently grafting the supervisor's memory onto a memory-less sub-agent instance. Sub-agents are usually long-lived singletons, so the first supervisor to delegate would set its memory on the shared instance and every later supervisor — and every direct invocation of that sub-agent — would keep reading and writing that first supervisor's memory. The supervisor's memory is now passed through the delegated run's request context, so inheritance applies for that invocation only and the sub-agent instance is never mutated. (#21692)

    Inherited memory is scoped to a single in-process delegation: it applies to the sub-agent the supervisor delegated to and not to agents that sub-agent delegates to in turn, and it is not carried across a durable run's suspend/resume boundary. A sub-agent that needs memory in its own right should declare it, as before.

  • Fixed observed durable run cancellation so calling cleanup() immediately after abort() no longer removes run state before terminal events and lifecycle callbacks are delivered. (#21527)

    Related to #21522

  • Tightened AgentConfig.tools typing so each entry must be an actual tool object. Previously a plain function such as tools: { myTool: () => realTool } passed type checking and then threw TOOL_INVALID_FORMAT at runtime, because the provider-defined tool member of the union is all-optional with an index signature. Provider-defined tools now require an id when used as a tools-map entry, mirroring the existing runtime check. Setting tools itself to a resolver function is still supported. (#21705)

  • Fixed MCP-exposed agents so nested tools receive elicitation and other MCP context. (#20549) (#21548)

  • Normalize instruction-file paths (AGENTS.md/CLAUDE.md/CONTEXT.md) to forward slashes in dynamic system-reminder injection. On Windows, node:path produced backslash-separated paths that leaked into the prompt reminders and the metadata used to avoid re-injection; paths are now identical across platforms and match the paths tool calls report. Windows filesystem APIs accept forward slashes, so file reads are unaffected. (#21071)

@mastra/ai-sdk@1.9.0

Minor Changes

  • Add version: 'v7' support to the AI SDK UI helpers. toAISdkMessages(), toAISdkStream(), handleChatStream()/chatRoute(), handleNetworkStream()/networkRoute(), and handleWorkflowStream()/workflowRoute() now accept 'v7' and return streams and messages typed against AI SDK v7, so apps on AI SDK v7 no longer need casts at the route boundary. (#21720)

@mastra/auth-auth0@1.2.2

Patch Changes

  • Fixed reading request headers from Express-style plain header objects so cookie-based auth providers no longer throw and fail with a misleading 401. (#21261)

    Related to #21253

    Also in: @mastra/auth-better-auth@1.1.4, @mastra/auth-clerk@1.2.3, @mastra/auth-cloud@1.2.4, @mastra/auth-google@0.1.1, @mastra/auth-neon@0.3.1, @mastra/auth-okta@0.2.1, @mastra/auth-studio@1.3.4

@mastra/auth-better-auth@1.1.4

Patch Changes

  • Resolve a default activeOrganizationId from the user's oldest existing membership when the stored better-auth session has no activeOrganizationId set. Nothing in a default sign-in flow calls the organization plugin's setActive, so the field was always null and org-scoped consumers saw users with no organization. (#21482)

    import { MastraAuthBetterAuth } from '@mastra/auth-better-auth';
    
    const mastraAuth = new MastraAuthBetterAuth({ auth });
    const user = await mastraAuth.authenticateToken(token, request);
    // Populated even when the sign-in flow never called `setActive`.
    console.log(user?.session.activeOrganizationId);

    The resolution is read-only and best-effort: the session row is not mutated, users with no memberships still authenticate, and a failed lookup falls back to today's behavior. Caveats: the resolved value is an inferred default, not an explicit user selection, and a session whose active organization was deliberately cleared via setActive is indistinguishable from one that was never set, so it also receives the default. A membership removed by an administrator stops being applied to new sessions within about a minute.

@mastra/braintrust@1.3.6

Patch Changes

  • Fixed suspended and resumed workflow runs appearing as two disconnected traces in Braintrust. Suspended and resumed workflow runs now appear in one Braintrust trace, with the resumed run nested under the span it was suspended from. As part of this change, Braintrust traces are grouped by the Mastra trace ID instead of a random ID, so multiple runs that share an explicitly provided trace ID now appear as one Braintrust trace. Fixes #20771. (#21047)

    Upgrade note: a workflow run that was suspended before this upgrade and resumed after it still appears as two Braintrust traces — the older half was grouped under a random ID that the new version cannot recover — and the resumed half may display without a root span. This affects only runs in flight across the upgrade; runs suspended and resumed on the same version are unaffected. If this matters for your traces, drain suspended runs before upgrading.

@mastra/clickhouse@1.15.2

Patch Changes

  • Fixed ClickHouse replication initialization rejecting pre-existing tables that use local engines. Initialization now warns that CREATE TABLE IF NOT EXISTS will leave those tables unchanged and continues creating any missing replicated tables. (#21538)

  • Fixed Studio metrics and logs detection for ClickHouse observability storage. Fixes #21435. (#21536)

@mastra/client-js@1.41.0

Minor Changes

  • Added Agent.readPlan() for loading submitted plan Markdown. (#21658)

    const agent = client.getAgent('agent-id');
    const plan = await agent.readPlan('.mastracode/plans/add-dark-mode.md');
  • Fixed the agent controller event types, which described payloads the server never sends. (#21739)

    KnownAgentControllerEvent was written by hand and had drifted from the controller. Narrowing on om_activation gave you an enabled boolean that does not exist, om_status a status string instead of the token windows, om_thread_title_updated a title instead of newTitle, and subagent_end only a toolCallId — its agentType, result, isError and durationMs were missing. usage_update typed its payload as unknown, so every consumer cast it. Seven events the controller emits (state_changed, command_exit, tool_suspension_cancelled, and the four remaining subagent_* events) were not typed at all and fell through isKnownAgentControllerEvent.

    isKnownAgentControllerEvent now returns true for those seven events as well. If you route unrecognised events to a fallback branch, they no longer reach it — give them a case in your switch or they are silently dropped.

    thread_created now delivers thread.createdAt and thread.updatedAt as Dates, the way the message_* events already did — the stream carries them as ISO strings.

    Payload drift is now a compile error instead of a wrong field at runtime, so handlers reading the old fields need updating.

    // Before: compiled, but `enabled` is always undefined
    if (event.type === 'om_activation' && event.enabled) { ... }
    
    // After: tsc rejects it; the event carries cycleId, tokensActivated, generationCount, …
    if (event.type === 'om_activation') { console.log(event.tokensActivated) }

Patch Changes

  • Updated observability trace response types to include derived span statuses. (#21450)

  • Fixed the agent controller event types to match what the server actually sends. KnownAgentControllerEvent is now derived from the wire type @mastra/core exports instead of a hand-written copy: the display_state_changed fields are no longer all optional, and the error event no longer claims its error may be a bare string. Runtime behavior is unchanged; only fallbacks written for those two type gaps become unnecessary. (#21761)

    import type { KnownAgentControllerEvent } from '@mastra/client-js';
    
    function onEvent(event: KnownAgentControllerEvent) {
      if (event.type === 'display_state_changed') {
        // before: event.displayState.isRunning ?? false
        const running = event.displayState.isRunning;
      }
      if (event.type === 'error') {
        // before: typeof event.error === 'string' ? event.error : event.error.message
        const message = event.error.message;
      }
    }
  • session.state() now accepts a threadId, so reopening a chat can load the durable task list for that specific thread. (#21545)

    const state = await session.state({ threadId: 'thread-123' });
  • Fixed the agent controller types so they match what the server actually sends. The REST types are now derived from the route contracts the server publishes instead of being maintained by hand, so they can no longer drift. (#21503)

    Three types were describing fields that never arrive over the wire:

    • AgentControllerAvailableModel.apiKeyEnvVar — the models route sends id, provider, modelName, hasApiKey and useCount only. The field is gone; reading it was always undefined.
    • AgentControllerThreadInfo — now the thread shape listThreads() actually returns (id, title, updatedAt, tags, state). It no longer claims resourceId and createdAt, which that route does not send.
    • createThread() and cloneThread() return the new CreateAgentControllerThreadResponse (id, title, resourceId, createdAt, updatedAt), which is a different shape from a listing entry.

    Only apiKeyEnvVar needs action on your side. If you read it to tell whether a model is usable, read hasApiKey instead:

    const models = await client.getAgentController('my-controller').listModels();
    
    // Before: typed string | undefined, undefined at runtime, so always empty
    const usable = models.filter(model => model.apiKeyEnvVar);
    
    // After
    const usable = models.filter(model => model.hasApiKey);

    The thread types need no migration: listThreads(), createThread() and cloneThread() each infer the shape their own route returns.

    PermissionPolicy, ToolCategory and AgentControllerTaskSnapshot are now re-exported from @mastra/core rather than redeclared, so the SDK and core can't disagree about them. AgentControllerActiveRun is now exported from the package root alongside the other agent controller types.

  • Preserve tool-call providerMetadata at the message-part level during client-tool continuations. (#21703)

    The stream reducers nested providerMetadata inside toolInvocation, but the server reads it from part.providerMetadata when rebuilding the prompt. As a result the metadata was dropped on the recursive request, and Gemini thinking models (e.g. gemini-3-flash-preview) failed the follow-up turn with Function call is missing a thought_signature in functionCall parts.

@mastra/cloudflare-sandbox@0.2.0

Minor Changes

  • Add a Cloudflare Sandbox provider that executes commands and writes workspace files through a deployed Sandbox Bridge Worker. (#21596)

@mastra/code-sdk@1.3.0

Minor Changes

  • Added opt-in discovery for MCP servers configured globally in Claude Code and Codex CLI. Enable the sources with mcp.claudeCodeGlobal or mcp.codexGlobal in Mastra Code settings. (#21598)

Patch Changes

  • Fixed custom command discovery loading Markdown files from node_modules. (#21680)

  • Stop an unread hook stdin from crashing the host process. A hook command that exits without reading its stdin closes the pipe mid-write, and the resulting EPIPE arrived as an unhandled socket error rather than a throw, so the surrounding try/catch never saw it and Node tore the process down. The socket error is now absorbed; the hook's real outcome still comes from its exit code. (#21796)

@mastra/deployer@1.60.0

Patch Changes

  • Fixed deployer analysis to use one deterministic external dependency set across analysis, bundling, and validation. Deprecated externals such as nodemailer, jsdom, sqlite3, and fastembed are now declared and installed as runtime dependencies instead of bundled. This can cause a one-time experiment-worker digest and build-key change for affected projects. (#21483)

  • Fixed mastra build pinning the wrong dependency version when the app and its parent workspace install different copies. The build now starts package lookup from the app directory, so the generated .mastra/output/package.json uses the app's installed version and the deployed server starts correctly. (#21213)

  • Fixed mastra dev bundling for createRoute imports in path-aliased route files. (#21804)

  • Improved pnpm build failures to name blocked dependencies and report the required allowBuilds configuration as a user error. (#21488)

@mastra/duckdb@1.6.2

Patch Changes

  • Fixed Studio metrics and logs detection for DuckDB observability storage. Fixes #21435. (#21536)

@mastra/editor@0.14.0

Minor Changes

  • Added authenticated-user execution and deterministic connected-account routing to ComposioToolProvider. Invoker-bound connections execute as the authenticated Composio user against the exact stored connected account, including accounts shared through Composio ACLs. (#21783)

    Use userIdResolver when application user IDs need mapping to Composio user IDs:

    import { MASTRA_USER_KEY } from '@mastra/server/auth';
    import { ComposioToolProvider } from '@mastra/editor/composio';
    
    const composio = new ComposioToolProvider({
      apiKey: process.env.COMPOSIO_API_KEY!,
      userIdResolver: ({ requestContext }) => {
        const user = requestContext?.getRaw(MASTRA_USER_KEY);
        if (!user || typeof user !== 'object' || !('id' in user) || typeof user.id !== 'string') return undefined;
        return user.id;
      },
    });

    Pinned caller-supplied connections now route to their exact account instead of allowing Composio to auto-select one.

Patch Changes

  • Fixed Composio tool results not being validated. Resolved Composio tools now keep the output schema supplied by Composio, so tool results are checked against it: real API responses (including null or extra fields) still pass, while structurally invalid output is rejected instead of being silently returned. This also lets Composio tools be used with APIs that require an output schema, like createStep(tool). (#21511)

  • Fixed editor-owned agent instructions failing silently. Agents configured with editor: { instructions: true } now throw a clear error instead of running with empty instructions when no published version is available in Studio. This affected agents that were never provisioned, only had a draft version, were deleted, had a published version with no instructions, or hit a storage error while loading. Fixes #21373 (#21395)

    Before: the agent ran normally with an empty system prompt.

    After: resolving or generating with the agent throws until a published version with instructions exists.

    // Agent definition — Studio owns the instructions:
    export const agent = new Agent({
      id: 'support-agent',
      editor: { instructions: true },
      model: 'openai/gpt-4o',
    });
    // Throws until a version is published in Studio:
    const agent = client.getAgent('support-agent', { status: 'published' });
    await agent.generate('hi');
    
    // Use status: 'draft' to run against the latest draft instead, without publishing:
    const draftAgent = client.getAgent('support-agent', { status: 'draft' });
    await draftAgent.generate('hi');
  • Keep agent snapshot updates as drafts until explicitly published. (#21528)

@mastra/evals@1.8.0

Minor Changes

  • Added an includeConversationHistory option to the Prompt Alignment scorer so multi-turn agent runs are scored in context. (#21683)

    Previously the scorer only saw the current turn. In a conversation a short reply like "A" has no meaning on its own, so the judge could not tell what the user asked for and scored a perfectly good response as misaligned. The scorer now optionally includes the prior turns from the agent's memory, uses them to interpret the current prompt, and still scores only the current response.

    Before

    const scorer = createPromptAlignmentScorerLLM({
      model: 'openai/gpt-5-mini',
      options: { evaluationMode: 'user' },
    });

    After

    const scorer = createPromptAlignmentScorerLLM({
      model: 'openai/gpt-5-mini',
      options: {
        evaluationMode: 'user',
        includeConversationHistory: true, // or { maxMessages: 6 }
      },
    });

    The option is off by default, so existing scores do not change. It only applies to agent runs, which are the runs that carry remembered messages. Fixes #21638

Patch Changes

  • Fixed LLM-judge scorers scoring an intermediate reply instead of the agent's final answer. When agent output contains multiple assistant messages (multi-step runs), scorers such as Prompt Alignment now evaluate the last assistant response that contains text. Fixes #21645 (#21686)

@mastra/factory@0.8.0

Minor Changes

  • Add per-repository worktree teardown commands and run them during terminal, explicit, and destructive Factory session cleanup. (#21564)

  • Made Factory session workspace resolution lazy. Resolving a session now returns the workspace immediately with a lazy sandbox handle; sandbox provisioning, repository materialization, branch checkout, and setup run in the background at session start (or on the first filesystem/sandbox operation) instead of blocking agent start. Storage reads during resolution are parallelized, failed background materializations are retried on the next use, and metadata-only resolutions such as thread-list polling never trigger sandbox work. (#21803)

  • Added org-visible Factory sessions: sessions store a visibility property derived from origin, org members can open org-visible sessions, and factory-ui shows session owners and access errors. (#21460)

  • Added a configurable allowlist of reviewer bots that can trigger GitHub review and comment notifications. Set MASTRACODE_GITHUB_AUTHORIZED_BOTS (comma-separated logins) or pass authorizedBots to GithubIntegration to trust bots beyond the built-in defaults; previously only coderabbitai[bot] and devin-ai-integration[bot] were accepted and every other bot was dropped without a log line. Bot logins now match case-insensitively and rejected senders are logged. Fixes #21621 (#21697)

  • Sped up new Factory agent sessions with warm repo base checkpoints. When a repository is connected, Factory now builds a base sandbox checkpoint (clone plus setup command) in the background, rebuilds it when pull requests merge to the default branch or pushes land there, and keeps it fresh via the periodic reconcile sweep. New sessions boot from the base checkpoint and skip the full clone and setup, falling back to the previous cold path when no checkpoint is available. (#21803)

Patch Changes

  • Speed up the local dev watch for the design system: pnpm dev:ui now rebuilds @mastra/playground-ui on save, so design-system edits show up in the Factory UI without a manual rebuild. pnpm dev:playground picks up the same watch. The watch starts from a full build and then skips type declaration emit on every rebuild, which brings each save from ~9s down to ~1.5s. (#21646)

    Declarations stay frozen at that starting build for the length of a dev session — run pnpm --filter @mastra/playground-ui build after changing a component's props. The published build is unchanged and still emits declarations.

    Also in: @mastra/playground-ui@50.0.0

  • Factory sessions can start before their sandbox is ready: resolving a session returns its workspace immediately, and background checkpoint-build failures now show up in logs instead of disappearing. (#21803)

  • Fixed session materialization timing being overwritten when sessions resume. The initial-materialize timestamp is now recorded once and preserved across idle-reap, checkpoint restore, and sandbox recreation, so time-to-first-materialize measurements reflect the true initial cost. Historical metrics captured before this fix are not backfilled. (#21520)

  • Prevent Factory handoff files from colliding across work items (#21763)

  • Added Factory session state to browser tabs and the sidebar, so a running session can be followed without switching to its window. (#21426)

    • Session tab favicons are color-coded: amber while initializing, green while the agent works, blue when it is your turn, red on failure.
    • Sidebar status dots now cover workspaces and user sessions alike, with Initializing / Working / Ready tooltips in the same three colors, so a tab and its sidebar row read the same.
    • Failures show on the favicon only; the sidebar has no error dot yet.
    • Tab titles show the session's identifier — #1567 for GitHub pull requests and issues, COR-210 for Linear — or the thread title for user sessions.
    • Board kickoff toasts gained a New Tab action, so a ready session opens without leaving the board.
    • Fixed a pinned session losing its sidebar slot when five other sessions were busy at once.
  • Fixed Linear issue reconciliation for issues that are not assigned to a project. (#21601)

  • Fixed workspace failures vanishing from the chat transcript. A workspace that failed to clone or start only flipped an internal flag that nothing rendered, so the session simply looked stuck with no reason given. The failure now appears as an error notice in the transcript — the same message the terminal already printed — for both the workspace_error and the failing workspace_status_changed event. (#21746)

  • Fixed the Factory chat transcript drawing the same content twice after coming back to a tab. While a run streams, leaving the tab drops the event stream and the transcript refetches on return: an assistant reply the server had persisted as its own step, and a steer whose live event was missed, both landed on screen a second time. The refetched window is now paired against what is already drawn — by message id, by tool call, then by the text itself — so it only inserts what is genuinely missing. (#21651)

    Also fixed a tool call rendering as two half-filled cards when a steer interrupted it: live tool state followed the newest assistant message instead of staying with the call it belongs to.

  • Resume a skill run that was aborted out from under it. (#21802)

    An aborted run was recorded as a terminal failure on the assumption that an
    abort is deliberate. In practice the dominant cause is the process going away
    underneath the run — an operator restarting the server — and the run stream does
    not say which happened. Cards were dead-ending at attempt 1 with nothing on the
    board to press, needing a human to nudge each one by hand after every restart.

    Aborted runs are now retried like any other interrupted work, still bounded by
    the existing attempt cap.

  • Stop Factory from waking itself on its own GitHub comments. (#21800)

    Factory recognised its own writes by comparing the event sender against
    GITHUB_APP_SLUG. That variable names the deployment's own self-hosted GitHub
    App, which is a different App than the one a Platform deployment posts as — and
    on such a deployment it is legitimately unset, so the check compared against
    undefined[bot] and never matched. Every self-loop guard silently failed open.

    The visible result: triage published its handoff comment, that comment came back
    through ingress, re-invoked triage, and cancelled the run that had written it —
    leaving the public verdict stuck at "Pending" while both runs reported success.

    The Platform integration now names the App it actually posts as, overridable
    with MASTRA_PLATFORM_GITHUB_APP_SLUG, and identity resolution is centralised so
    an unresolved identity is reported as unknown rather than collapsing into
    "not Factory".

  • Let a Factory run finish its stage when the previous role handed off in the same (#21802)
    session.

    factory_transition_work_item re-checked its authority at execution time by
    comparing the live run binding against the binding row that existed when the
    tool was built, requiring the same row id. But handing the next role its turn in
    an existing session legitimately rotates that row: the previous role's binding is
    revoked and a new one is issued for the same session and the same work item.
    Tools built for the earlier role stay live across that rotation, so they were
    keyed to a row that the handoff itself had just replaced.

    The visible result: planning produced a complete plan, called its terminal
    transition to execute, and was refused with "Factory agent binding is
    unavailable, revoked, or no longer matches this session." The item stopped in
    Planning with the plan written but never advanced, and the decision that carried
    it reported success. Every leg that continues an item in an existing session —
    planning after triage, and the review-feedback wakes — failed the same way.

    Authority is now the work item the session is bound to rather than the
    individual binding row, so a rotation no longer strands the run it exists to
    start. Re-pointing a session at a different work item is still refused.

  • Start the implementation run when a work item enters Building. (#21802)

    Building was the one stage on the Work board with no entry rule, so an item
    arriving there stopped: the plan was approved and nothing picked it up until
    somebody pressed Build by hand. Every other stage advances itself, which made
    Building the single manual step in an otherwise continuous path from intake to
    review.

    The run it starts carries a prompt rather than activating a skill. Skills exist
    here to define a handoff — a terminal message later rules match on to decide
    what happens next — and Building already has one: it ends by opening a pull
    request, which arrives as its own event and raises the Review card. Rules could
    previously only express "invoke this skill", so the decision vocabulary now
    accepts a prompt as the alternative to a skill name.

  • Credit the reporter as a co-author on work their issue caused (#21802)

    When Factory builds a fix for a GitHub issue, the build prompt now asks for a
    Co-Authored-By trailer naming the person who reported it, so the reporter shows
    up as a contributor on the pull request rather than only in the issue thread.

    Only GitHub issues qualify. A Linear card stamps a display name and a manual card
    stamps nothing, and neither resolves to the GitHub account a trailer needs, so
    those are left uncredited rather than credited to nobody. Issues Factory filed
    itself are skipped.

    Intake already stamped the reporter's login but the stage rules could not see it;
    the intake-stamped metadata now reaches rules that run on a stage.

  • Stop reporting a skill kickoff as successful when it was queued onto a run that was already ending. (#21802)

    Signals sent into an active session settle as deliver, which acknowledges routing but promises nothing about execution. If the in-flight run finished before draining its queue, the prompt was dropped: no turn started, no error surfaced, and the decision was marked succeeded while the work item sat in its new stage with nobody working on it. The dispatcher now confirms the signal actually landed in the thread and retries the decision when it did not, so the next attempt finds the session idle and takes the instrumented wake path.

  • Dismiss runs still parked on a work item when it reaches a terminal stage. A merged or closed pull request cannot answer a suggested run, so the card no longer keeps asking. (#21802)

  • Check who sent a GitHub comment or review before letting it wake an agent, and ingest default-branch push events. (#21800)

    The sender gate listed event kinds under names the webhook classifier never produces, so the identity check that keeps untrusted commenters from waking Factory agents was skipped for every comment and review event. The gate now names the kinds the classifier actually emits. Separately, push events were dropped by the event filter before ingestion; they are now ingested and forwarded to Factory's event pipeline so downstream consumers (such as the upcoming warm base-checkpoint refresh) can observe default-branch pushes.

  • Ingest pull_request.opened from the Platform event poller so a newly opened pull request mints its Review card. The poller forwards an allow-list of events to the rules engine, and opened was missing from it — so on deployments without a direct webhook (the only path a local deployment has), a pull request the factory authored itself was never reviewed. (#21802)

  • Keep kickoff skill resolution off the sandbox so clicking Review on a board card no longer blocks on full sandbox provisioning. Project skill roots (.claude/skills, .agents/skills, <configDir>/skills) are guarded while the session sandbox is unmaterialized — discovery reports them empty instead of forcing materialization — and a skills rescan fires automatically once materialization completes so repo-local skills become visible. Bundled Factory skills (e.g. factory-review) resolve from local disk in milliseconds. (#21802)

  • Stop the GitHub event worker from crashing when it is constructed before source-control storage is initialized. (#21801)

    workers() dereferenced the integration's source-control storage eagerly while building the reconcile worker, but that storage is only attached later by versionControl.initialize. A deployment that constructs workers first crashed with "source-control storage has not been initialized". The worker now receives a lazy handle that resolves the storage slices at call time, once the worker is actually running.

  • Deliver GitHub review feedback to the factory rules on the polling path. Submitted reviews and new pull request comments were dropped before the rules engine ran, so the agent that authored a branch was never woken when a reviewer asked for changes. (#21802)

  • Route pull request comments to the agent that authored the pull request, and stop provenance from branding commenters as Factory. (#21802)

    Comments on a PR arrive as issue_comment with issue.pull_request set, and the ingress explicitly dropped them. That closed the most common feedback path of all: on a Factory-authored PR, GitHub refuses a formal --approve/--request-changes verdict from the account that opened it, so the review skill falls back to gh pr comment — which was discarded. External review bots leaving plain comments were dropped for the same reason.

    A new pullRequestCommentCreated rule event now carries those comments, reading the pull request from the issue payload so provenance binds the comment to the authoring Work item rather than mistaking the number for an issue's. The default rule sends a high-priority sendMessage to the work role, which wakes an idle session. Factory's own comments are ignored, because factoryAuthored cannot distinguish the Work role from the Review role and reacting to them would let an agent wake itself in a loop.

    Separately, factoryAuthored was derived from PR provenance for every event, which proves the pull request came from Factory, not the sender of the event. Any human or review bot commenting on a Factory-authored PR was therefore marked as Factory. Provenance-based attribution is now skipped for events whose sender is responding to the PR — comments, submitted reviews, and re-requested reviews — where only the app login identifies Factory.

  • Wait for the run that swallowed a kickoff, instead of racing it. (#21802)

    A signal queued onto an already-running turn can be dropped when that turn ends
    before draining its queue. That case was detected correctly and then handed to
    the generic exponential backoff, which spends all five attempts inside about
    thirty seconds — while the run it is waiting on takes minutes. Every attempt
    landed on the same busy session and the card gave up roughly ten times too early.

    The dispatcher now waits for the in-flight run to end and redelivers into the
    freed session, which is the event that actually resolves the condition. The
    decision settles within its original lease without spending the retry budget, and
    a redelivery that is dropped again still goes back on the queue.

  • Only credit reporters who are real GitHub accounts (#21802)

    The issue poller stamps a placeholder login when GitHub returns no author, which
    would have become a Co-Authored-By trailer crediting an account nobody owns.
    Reporter credit now requires a login that matches GitHub's grammar.

  • Re-review a pull request when a push lands while its card is still in Reviewing. (#21802)

    A push to a card already sitting in Reviewing was dropped rather than deferred:
    the rule returned nothing, and once the in-flight pass finished it transitioned
    to Done having reviewed code that was no longer current, with no record that
    newer commits had arrived. That is the exact ordering the review loop produces —
    a review asks for changes, the authoring agent pushes a fix, and the fix lands
    before the card finishes leaving Reviewing.

    Only Intake now suppresses the re-review. A push during Reviewing re-enters the
    stage, which supersedes the stale pass: the stage rule already cancels the run
    in flight and selects the right skill for the entry it sees.

    Transition decisions carry a reenter flag for this, since a transition to the
    stage an item already holds is otherwise inert — the common case is a board
    being corrected into a state it already has, not work that needs restarting.

  • Credit the author on review follow-up pull requests (#21802)

    When a review pass ships mechanical fixes as a follow-up pull request, those
    commits now carry a Co-Authored-By trailer for the human whose work they build
    on — the reviewed pull request's author, or, when that author is a bot, the
    reporter of the issue the pull request closes.

  • Carry pull request review feedback back to the agent that wrote the code. (#21802)

    A changes_requested review is meant to wake the authoring work session, but
    when the pull request carried no provenance the event resolved to the pull
    request's own Review card. addressReviewFeedback deliberately refuses to act
    on the review board — a Review card reacting to its own posted review would loop
    the reviewer against itself — so the wake was dropped and the author never heard
    about the feedback.

    Review and pull request comment events now follow the linked card's
    parentWorkItemId back to the item that authored the pull request, so the
    existing guard becomes true for the right item instead of never. A pull request
    card with no authoring item still emits nothing.

  • Close the work/review loop: a review that requests changes now wakes the agent that authored the pull request. (#21802)

    pull_request_review webhooks were accepted and classified urgent, but no matching rule event existed, so the delivery was dropped after classification and the authoring agent was never told. PR subscriptions did not cover this — they sync PR activity into a thread's notification inbox for the agent to read on its next turn, but nothing starts that turn.

    A new pullRequestReviewSubmitted rule event maps pull_request_review/submitted, and the default rule sends a high-priority sendMessage to the work role, which wakes an idle session. Only changes_requested fires; approved and commented stay quiet, and the Review card that posted the review never reacts to its own output.

  • Send Factory's own pull requests straight to Review. (#21802)

    A pull request entered Review only when its author passed a repository-collaborator
    permission check. A GitHub App bot is never a collaborator, so every pull request
    Factory opened itself scored as untrusted and parked in Intake, waiting on a human
    click — the exact opposite of the intent, since those are the pull requests whose
    provenance Factory knows best.

    Factory authorship is now its own trust signal for pull requests: the branch came
    from a Work run this Factory dispatched. Issues are deliberately unchanged, because
    auto-triaging an issue Factory opened is a self-loop with no upside.

  • Stop a running local Factory session from wedging when its checkout directory disappears. A tool spawned into a removed directory fails with spawn /bin/sh ENOENT — an error that names the shell rather than the sandbox — so it was never recognized as a dead sandbox, and every later filesystem or command tool (including GitHub token refresh) failed the same way for the rest of the run. A missing working directory is now treated as the local equivalent of a destroyed sandbox: if the session is still live, the revival ladder rebuilds the checkout and retries the command; if the session was retired (retirement deletes the checkout on purpose), the run fails fast with a clear retirement error instead of resurrecting the retired checkout — before provisioning anything, so a retired session never consumes a sandbox or fleet budget slot, and a sandbox already mid-build when retirement lands is torn down rather than left bound to a dead session. A missing command reports the same ENOENT code, so the working directory is probed to tell the two apart and healthy sandboxes are never rebuilt for an unknown command. (#21803)

  • Report what actually happened to a Factory run. A human dragging a card on the board now arms the item, so the run it asks for starts instead of parking for approval, and a run that dies mid-flight — a provider error, or a cancellation by the next decision — is recorded as failed on the decision instead of reported as a success. (#21802)

  • Factory sessions now revive a sandbox that dies mid-session instead of erroring the turn. When a command fails with a destroyed-sandbox error (for example after idle garbage collection), or with an exec-transport error whose connection never opened (so the command provably never started), the session drops the dead handle, re-runs the provisioning pipeline (reattach, checkpoint-seeded provision, or fresh clone), and retries the command once. Transport errors where the command may have already run are surfaced instead of replayed, so side effects like git commit cannot execute twice. Concurrent failures coalesce onto a single revival. (#21803)

  • Retire a parked proposal when the run it asked for starts anyway. Approving a (#21802)
    proposal mints a fresh decision rather than dispatching the parked one, so the
    original stayed proposed forever and the card kept asking to start a run that
    had already finished. The dispatcher now dismisses proposals for the same work
    item and role as any invokeSkill it dispatches, so the waiting badge only ever
    marks a loop that is genuinely stopped.

  • Make the board honest about runs it is waiting on, and about clicks that fail. (#21766)

    Four gaps closed on the work board:

    • A card click that failed while refreshing workspaces before starting a run did
      nothing at all — no run, no error. It now surfaces the failure instead of
      swallowing it, so an expired session reads as an expired session.
    • A run a rule proposed could not be approved from the card. The card menu now
      offers it.
    • After a plan was approved, the Building run became unreachable from the card,
      leaving the item stranded mid-loop.
    • A card with a proposed run looked idle. It now says it is waiting on someone,
      with the approval inline.
  • Stop showing "Linked card could not be filed" when nothing failed. (#21766)

    A linked-card decision that already succeeded is deliberately reset to retry
    when its card is rematerialized, so the card gets re-filed. The board read any
    retry as "already failed at least once" and put an error on the card, so a
    routine replay looked like a broken automation — 16 cards were showing a failure
    nobody caused.

    A card now reports an error only when the effect has actually been attempted or
    left an error behind. A replay reads as what it is: the work it is doing.

  • Fixed Factory completion so moving a GitHub issue to Done marks it as pending close and removes its remaining triage status labels. (#21515)

  • Deliver GitHub pull request signals to the session that actually owns the subscribed thread, and skip subscriptions whose thread this deployment does not hold. (#21800)

    A subscription records the Factory project as its resource, but an unscoped session registers under its own id, so delivery looked for the thread under a resource that did not own it and failed with "Thread not found" on every matching event. Delivery now reads the thread from storage to find its owning resource. A subscription naming a thread that is absent is skipped rather than failed, so a pull request's events reaching a deployment that never owned the thread no longer fabricate a session or retry in a loop.

  • Fixed observational memory in Factory web user sessions ignoring the stored memory settings. Sessions created from the web UI now start with the observer and reflector models, thresholds, and attachment preferences saved in your memory settings, instead of falling back to the built-in default model — which failed with a missing API key error when that provider was not configured. (#21423)

  • Fixed restarting a review after deleting its thread. It no longer fails with "git clone failed: a branch named ... already exists". Reused Platform sandboxes now delete the previous session's local branches when they are recycled. A new session for the same branch starts fresh from the base branch. Branch checkout also recovers from leftover or broken branch refs instead of failing the workspace. (#21268)

  • The Review board now has a Canceled column. A pull request closed without merging used to reappear in Intake — the queue of pull requests still waiting for review — carrying a "Canceled" chip to explain why it looked out of place. It now sits in its own column. (#21323)

  • Fixed the Factory session favicons being nearly invisible in light-themed browsers: the Mastra mark now switches to black on light and white on dark, matching the default favicon, and the state dots use the design system's light-theme accents so amber, green, blue, and red stay legible on a white tab strip. (#21510)

  • Improved Factory stage handling so run routes and board columns stay aligned with the rule stage vocabulary. (#21516)

  • Smoothed out the chat transcript. A streamed reply now reveals at a steady pace instead of arriving in clumps, and a tool card that turns up while the agent is working fades in rather than popping onto the page. Both stop for readers who ask for reduced motion. (#21499)

  • Improved chat scrolling in the factory. Sending a message now scrolls once and parks it near the top with room under it, and the view stays on the agent's newest output — tool progress, subagents, the streamed reply — instead of standing still or jumping back up to what you just sent. (#21523)

    Scroll up to read back and the chat stops following. Return to the bottom and it picks the stream up again. The jump-to-latest button no longer flickers when you send a message.

    The room under the live turn is released when the run ends, so a finished conversation settles against the composer instead of leaving most of the window blank.

  • Board cards now show one status line instead of stacking several. A card reports what you just triggered, then what a rule is doing on its own, then what a click will do — one thing at a time, in that order. (#21323)

    Automatic actions say what they do rather than where they sit in a queue. A card reads "Starting an automated run…" while a rule works, and "Automated run could not start" when it gives up, with the raw error one hover away next to Retry. An action the server is still re-attempting says "— retrying…", so a card looping through retries no longer looks like one starting for the first time.

    Unfiled GitHub and Linear items now use the same card as filed work. Clicking anywhere on the card starts its default run and reports "Starting run…" while that resolves, instead of looking inert. A link to the issue or pull request sits beside the title, and the remaining actions are in the card's actions menu.

    A resting card also shows less. The click hint and the actions menu fade in when you point at the card or reach it with the keyboard, the hint shares the author's line instead of taking a row of its own, and labels are shorter. Touch screens have no hover, so they keep both visible.

  • The turn-end filesystem capture no longer blocks agent turn completion. Readers of the persisted workspace file listing wait up to 10 seconds for the in-flight capture to observe the just-ended turn's files; if a capture takes longer, a reader can temporarily receive the previous listing. (#21679)

  • Platform GitHub event polling is now scoped to the repositories linked to a Factory project. Previously the worker polled every repository the underlying GitHub App installation exposed, which for customers who grant broad org access meant hundreds of unnecessary requests per polling cycle. With this change, no polling happens for repositories that are not linked to a project, and repositories added or removed from a project are picked up automatically on the next polling cycle — no worker restart or additional configuration required. (#21772)

  • Fixed how the Factory chat transcript reads agent controller events. It branched on an om_activation.enabled flag the controller never sends, and cast token usage and memory progress into hand-written shapes that had drifted from the streamed payloads. Both now read the shapes the controller actually emits, so the status line and memory rings stay correct as those payloads evolve. (#21739)

  • Fixed model packs so each user can set a default for new interactive Factory chats while preserving thread-specific pack and model choices, refreshing edited packs, and leaving Factory work runs unaffected. (#21762)

  • Fixed Linear intake so issues only land in the Factory project their Linear project is routed to. Previously, opening any board pulled in every selected Linear project's open issues and auto-triaged them there, repeating for each Factory project you viewed. (#21698)

    Routing Linear projects

    In Settings › Intake, each selected Linear project now picks the Factory it feeds. A project left unrouted no longer feeds any board, and boards only show the Linear intake feed for projects routed to them. Organizations with a single Factory keep working with no configuration.

    Deleted cards stay deleted

    Removing an intake card now also clears the stored routing decision behind it, so it no longer reappears on the next intake poll.

    Fixes #21614

@mastra/inngest@1.8.7

Patch Changes

  • Fixed Inngest workflow resumes failing when persisted state grows too large by restoring resume state from storage instead of copying it into events. (#21549)

  • Fixed durable agent resumes to restore request context and continue the original trace after suspension. (#21566)

  • Fixed durable Inngest streams retaining events after consumers unsubscribe and prevented internal workflow watch events from filling replay caches. (#21572)

@mastra/langsmith@1.3.11

Patch Changes

  • Added TTL-specific Anthropic cache-write token details alongside the aggregate LangSmith usage value. (#21563)

@mastra/mcp@1.17.0

Minor Changes

  • Added elicitation support on the 2026-07-28 protocol revision using the spec's multi round-trip mechanism, completing the opt-in protocolVersion support. (#20931)

    Server — tools keep the same promise-shaped API on both eras:

    execute: async (inputData, options) => {
      const answer = await options.mcp.elicitation.sendRequest({
        message: 'What is your favorite color?',
        requestedSchema: { type: 'object', properties: { color: { type: 'string' } } },
      });
      // ...
    };

    On a 2026-07-28 request, the tool call first returns an input_required result. After the client answers, the call retries with the answer attached. The tool function re-executes from the top on each retry, so keep side effects idempotent (or place them after the last elicitation) and keep the order of sendRequest() calls deterministic. Legacy connections keep the existing push-based elicitation/create flow unchanged.

    Client — a handler registered with elicitation.onRequest() now fires on both eras: on 2026-07-28 connections, embedded elicitation requests are dispatched through the same handler and the originating tool call retries automatically. The warning that elicitation only works on legacy connections is removed.

  • Added per-server time budgets and duration metrics to MCP discovery methods. (#21560)

    const { tools, errors, durations } = await mcp.listToolsWithErrors({
      perServerTimeoutMs: 3_000,
    });
  • Added opt-in support for the stateless MCP protocol revision 2026-07-28 behind a protocolVersion flag on both MCPServer and the MCP client. Omitting the flag keeps today's behavior unchanged. (#20929)

    Server

    const server = new MCPServer({
      name: 'My Server',
      version: '1.0.0',
      tools: { weatherTool },
      protocolVersion: '2026-07-28',
      cacheHints: {
        'tools/list': { ttlMs: 60_000, cacheScope: 'private' },
      },
    });

    With the flag set:

    • One HTTP endpoint serves both protocol eras: 2026-07-28 clients are served natively (stateless), and legacy clients are served through an automatic stateless fallback.
    • startStdio() serves both eras, selecting the era from the connection's opening exchange.
    • Tool, prompt, and resource change notifications also reach 2026-07-28 clients through subscriptions/listen.
    • Tool log messages honor the caller's per-request logLevel opt-in.
    • Optional cacheHints advertise ttlMs / cacheScope on cacheable results such as tools/list.
    • startHTTP() continues to enforce configured host and origin guards. Session and handler-lifetime transport options fail with a clear error instead of being ignored.

    Client

    const mcp = new MCPClient({
      servers: {
        weather: {
          url: new URL('https://example.com/mcp'),
          protocolVersion: 'auto', // probe, fall back to legacy
          // or '2026-07-28' to pin the revision and fail loudly when unavailable
        },
      },
    });

    Elicitation handlers currently only fire on legacy connections; support for the 2026-07-28 input-required mechanism ships separately.

@mastra/memory@1.27.0

Minor Changes

  • Added continuationHints to observational memory configuration, so an agent that drives its (#20665)
    own control flow can stop memory from proposing what it says next.

    <current-task> and <suggested-response> were always requested and there was no way to turn
    them off. A <suggested-response> is injected into the agent's context and the continuation
    reminder tells the agent to follow it, which makes memory a second controller competing with
    the agent's own. Pass false to disable both sections, or an object to disable them
    individually — keeping <current-task> while dropping <suggested-response> is the common
    case.

    import { Memory } from '@mastra/memory';
    
    const memory = new Memory({
      options: {
        observationalMemory: {
          observation: { continuationHints: { suggestedResponse: false } },
          reflection: { continuationHints: false },
        },
      },
    });

    Disabling a section removes it fully: the Observer and Reflector no longer describe or
    reference it, and a previously stored hint stops being injected into the agent's context once
    both observation and reflection disable it.

    Defaults are unchanged — existing configurations keep both sections enabled.

Patch Changes

  • Added memory.settled(), which waits for background memory work to finish. Observational memory runs observation and reflection cycles in the background after an agent run returns, and those cycles kept writing to the database after callers had closed their storage connection. Await memory.settled() before closing a store you own. Fixed the observational memory reflector repeating compression attempts that could not succeed: it now stops as soon as an attempt returns the same result as the previous one, instead of always working through the full retry ladder. This cuts the model calls and database statements a single reflection produces. Fixes #21617 (#21708)

  • Log discarded degenerate observer/reflector output so failures are diagnosable. When observational memory's degenerate-repetition detector trips, the raw model output was previously thrown away — the error "Observer produced degenerate output after retry" gave no way to tell a real repetition loop apart from a detector false-positive on legitimately repetitive content. Detection sites now log bounded diagnostics (length, duplicate-window ratio, most-repeated window, head/tail snippets) to the OM debug log, and the thrown observer errors include a compact version of the same diagnostics. (#21807)

  • Fixed schema-based working memory so that null consistently deletes a field. Previously, a null only removed a field that already existed: on the very first write, or inside a nested object created for the first time, the null was stored literally. This mattered because strict-mode model providers pad every field they are not updating with null, so a first write could be saved as { "role": null }. Working memory updates are also no longer stored by reference to the object passed in. (#21687)

  • Fixed observational memory removing Markdown link labels from the observation (#20665)
    context given to agents. Links shared in observations previously collapsed to
    bare, unlabelled URLs; their label text is now preserved. Semantic tags are
    still stripped and collapsed-item markers behave as before.

  • Improved observational memory step latency by reusing loaded records and token counts across repeated status checks. (#21562)

  • Fixed observational memory status updates so they remain available to live clients without creating standalone messages in stored conversation history. (#21604)

  • Added regression coverage for resource-scoped observational memory: context loaded from a resource's other threads is accepted on the current thread, keeps its original thread ID, and is never re-saved onto the current thread. (#21702)

@mastra/mysql@0.8.0

Patch Changes

  • Fix the alterTable existing-column probe reading the wrong information_schema field casing. MySQL returns result fields with uppercase keys through mysql2, so the probe's existing-column set was always empty and every warm boot re-ran 107 ALTER TABLE ADD COLUMN statements that failed with ER_DUP_FIELDNAME and were silently swallowed, taking metadata locks on production tables for nothing. The probe now reads whichever key casing is present. Measured on docker mysql:9.7: warm init drops from 326 to 109 client-server round trips and issues zero ALTER statements. (#21633)

  • Cut warm initialization from around 110 client-server round trips to single digits with an init-scoped schema snapshot, on top of the column-probe casing fix that removed the ALTER TABLE storm. Three information_schema reads at the start of init() now answer table, column, and index existence locally; createTable, alterTable, createIndex, and hasColumn consult the snapshot and maintain it as objects are created, and the memory domain's raw CREATE INDEX for idx_om_lookup_key consults it too instead of raising and swallowing ER_DUP_KEYNAME on every boot. The snapshot lives for exactly the init window and is cleared in a finally, so runtime callers keep querying the live catalog. Measured on docker mysql:9.7: warm init 109 to 111 round trips down to 7 (6 excluding measurement scaffolding), cold init 253 down to 153 or 154 across runs, with an identical cold-init table and index census before and after. (#21634)

@mastra/observability@1.17.1

Patch Changes

  • Fixed the __truncated marker on span data. It now counts only the fields dropped by the object-key limit, so traces no longer report more omitted keys than were really dropped. Values beyond the limit are no longer read. (#21630)

  • Fixed span serialization so internal tracing fields are removed only from framework-owned payloads while preserving user data with the same key names. (#21332)

  • Advertise quota-pause support to the Mastra platform. MastraPlatformExporter now sends x-mastra-observability-capabilities: quota-pause-v1 on every request (batch uploads for all five signal types, plus the traces recovery probe), letting the platform respond with 402 Payment Required to clients that understand the quota-pause contract while shielding legacy clients from retry loops. No configuration change is required: (#21447)

    import { MastraPlatformExporter } from '@mastra/observability';
    
    const exporter = new MastraPlatformExporter({
      accessToken: process.env.MASTRA_PLATFORM_ACCESS_TOKEN,
    });
  • Fixed Anthropic cache-write cost estimates by applying TTL-specific rates without double counting aggregate tokens. (#21563)

@mastra/otel-bridge@1.5.1

Patch Changes

  • Fixed workflow traces breaking apart after suspend and resume when using OtelBridge. A resumed workflow run now continues the OpenTelemetry trace it started in instead of starting a brand-new one, even when the resume happens in a different process. This restores the trace continuity introduced in #12276 for setups that route spans through OpenTelemetry. Fixes #20771. (#21047)

@mastra/otel-exporter@1.3.9

Patch Changes

  • Added TTL-specific Anthropic cache-creation token attributes alongside the aggregate OpenTelemetry attribute. (#21563)

@mastra/pg@1.21.0

Patch Changes

  • Stop a dropped Postgres connection from killing the process while a client is checked out. (#21765)

    PgFactoryStorage attached an error listener to the pool, but pg only routes pool-level errors for idle clients — it hands ownership of a client to the borrower for the duration of a checkout. A backend restart or network blip that landed on a client mid-transaction therefore reached an emitter with nothing listening, and Node escalated it to an uncaughtException that took the whole server down (Connection terminated unexpectedly at pg/lib/client.js), even though the idle siblings were logged and discarded cleanly.

    Pools created by PgFactoryStorage now attach a listener once per physical connection as it is established, so a client stays covered while borrowed. While the client is idle the pool's own listener already reports the failure, so the extra listener stays quiet and a dropped connection is announced once, as the right thing. The pool still discards the failed connection and reconnects on the next checkout; the failure is now logged instead of fatal. Caller-supplied pools are left untouched, as before.

  • Improved workflow run list performance in @mastra/pg when filtering by workflow name. The default index avoids sorting the ordered result query for workflows with large run histories. Paginated requests still use a separate count query. (#21308)

  • Improved PgVector upsert performance when writing many vectors at once. Batches are now written with a small number of multi-row inserts instead of one insert per vector, which reduces database round trips and connection pool usage during RAG and memory ingestion. (#21719)

  • Make listWorkflowRuns status filtering indexable on Postgres. The status predicate previously wrapped every snapshot in a regexp_replace(snapshot::text, ...)::jsonb sanitization step, which forced a sequential scan over the whole mastra_workflow_snapshot table. On jsonb snapshot columns Postgres already rejects the problematic Unicode escape sequences at insert time, so the sanitization was a no-op there and the query now uses a plain snapshot->>'status' comparison backed by a new default expression index on (workflow_name, snapshot->>'status', "createdAt" DESC). Legacy tables whose snapshot column is still json or text keep the sanitizing path. (#21684)

@mastra/platform-workspace@1.3.0

Patch Changes

  • PlatformSandbox restarts use the current sandbox connection after the previous sandbox is deleted or the platform does not return an instance URL, so later commands do not hit a stale sidecar. (#21798)

  • Added fallback checkpoint forwarding for Platform sandboxes so the workspace proxy can seed fresh sessions without changing their primary recovery key. (#21803)

    const sandbox = new PlatformSandbox({
      id: 'session-42',
      seedCheckpointName: 'repo-base',
    });
  • PlatformSandbox now restarts a timed-out sidecar health probe on the next command instead of falling back to the slower lease-based exec path for the sandbox's lifetime. If the in-sandbox sidecar was just slow to boot, later commands recover the fast private-network transport automatically. (#21798)

  • Declared checkpoint support (supportsCheckpoints) so checkpoint-based features like warm base checkpoints and boot-from-checkpoint know snapshots are real. (#21798)

    // Gate checkpoint-dependent work on the provider's capability flag.
    if (sandbox.supportsCheckpoints) {
      await sandbox.snapshot(); // persists a checkpoint that can seed a later boot
    }

    Also in: @mastra/railway@0.6.0

@mastra/playground-ui@50.0.0

Minor Changes

  • Added filled and ghost destructive Button variants for dangerous actions. (#21790)

    <Button variant="destructive">Delete</Button>
    <Button variant="destructive-ghost">Remove</Button>
  • Streamed replies now arrive one word at a time instead of in bursts. (#21499)

    Chunks reach the browser unevenly — a proxy flushes, a tool call ends, the model changes pace — so a reply used to lurch: ten words at once, then nothing for a fifth of a second. MarkdownRenderer now paces a reply marked streaming itself, revealing it one word at a time at the speed the reply is actually arriving. Bursts and gaps stop reaching the page, and a change of pace reads as one rather than as a jolt.

    <MarkdownRenderer streaming={part.state === 'streaming'}>{part.text}</MarkdownRenderer>

    Each word fades in as it lands, and code fades in whole — a fence or a piece of inline code appears with its background rather than a token at a time. A word fades in once and only once, so a paragraph never flickers as the rest of the reply arrives behind it.

    A thread opened from history renders whole. A reply opened part-written joins it rather than retyping it, and what was already on screen when you opened it stays put: only the words landing from then on animate. Readers who ask for reduced motion get the text at once, unanimated.

  • Improved MessageScroller so chat transcripts follow the stream. With autoScroll on, the reader is carried with the newest output while they sit at the end, and a new user turn brings them back to it. Only scrolling away stops the following — content growing under the reader, or landing above them, no longer moves them or flashes the jump-to-end button. (#21523)

    A turn opening animates the scroll only for a reader who had scrolled away. Someone already at the end is carried by whatever the turn grows under itself, so a surface can reserve room under a live turn and let its own transition set the pace:

    <MessageScrollerProvider autoScroll>
      <MessageScrollerViewport>
        <MessageScrollerContent>
          {/* the viewport is a size container, so a live turn can reserve a share of it */}
          <div className="min-h-[70cqh]">{liveTurn}</div>
        </MessageScrollerContent>
      </MessageScrollerViewport>
    </MessageScrollerProvider>
  • Added token and estimated cost totals to full trace details in Studio. Opening a trace now shows input tokens, output tokens, and estimated cost in both the trace side panel and the dedicated trace page, including when the trace is opened from a direct link outside the loaded list. Subtrace panels omit these values because they represent totals for the full trace. (#21541)

  • Added an "lg" size variant to sidebar navigation items for taller (36px) rows. (#21509)

  • Replaced the task list's progress bar and "2/4 completed" label with a compact status strip in the header. One bar per task, colored by state (green completed, orange in progress, grey pending), with the exact count on hover. Frees the vertical space the progress bar used to take. (#21507)

    <TaskList> is unchanged. The strip now derives everything from the tasks, so TaskListCount and TaskListCountProps are removed, and TaskListProgress takes the tasks instead of a completed/total pair. Both were reachable from @mastra/playground-ui/components/ai/task-list.

    There is no replacement for TaskListCount — the count lives in the strip's tooltip. Callers of TaskListProgress pass the tasks they already render:

    const tasks = [
      { id: '1', content: 'Inspect code', activeForm: 'Inspecting code', status: 'completed' },
      { id: '2', content: 'Add tests', activeForm: 'Adding tests', status: 'in_progress' },
      { id: '3', content: 'Build package', activeForm: 'Building package', status: 'pending' },
    ];
    
    // Before
    <TaskListCount completed={1} total={3} />
    <TaskListProgress completed={1} total={3} />
    
    // After
    <TaskListProgress tasks={tasks} />

Patch Changes

  • Improved trace details with readable times, precise hover values, responsive layouts, and clearer statuses. (#21660)

  • Improved plan action hierarchy, kept expand and collapse labels on one line, and hid expansion for short plans. (#21658)

@mastra/posthog@1.3.3

Patch Changes

  • Added TTL-specific Anthropic cache-creation token properties alongside the aggregate PostHog usage value. (#21563)

@mastra/rag@2.6.0

Minor Changes

  • Added serialize() and GraphRAG.deserialize() so a knowledge graph can be saved and restored instead of rebuilt on every process start. Building a graph compares every chunk against every other chunk, which is slow for large document sets; now you can do that work once and reload the result. (#21704)

    // Before: the graph had to be rebuilt every time
    const graphRag = new GraphRAG(1536, 0.7);
    graphRag.createGraph(documentChunks, embeddings);
    
    // After: build once, save the snapshot, and reload it later
    const snapshot = graphRag.serialize();
    await writeFile('./graph.json', JSON.stringify(snapshot));
    
    const restored = GraphRAG.deserialize(JSON.parse(await readFile('./graph.json', 'utf8')));
    restored.query({ query: queryEmbedding, topK: 10 });

    A snapshot is plain JSON, so you can store it in any database, file, or cache you already use. Loading a snapshot that does not match the graph's embedding dimension now fails immediately with a clear error instead of later during a query. Closes #3926.

@mastra/railway@0.6.0

Patch Changes

  • Added support for booting new Railway sandboxes from a fallback checkpoint while keeping future snapshots isolated to the sandbox's own checkpoint. (#21803)

    const sandbox = new RailwaySandbox({
      checkpointName: 'session-42',
      seedCheckpointName: 'repo-base',
    });

@mastra/react@1.4.4

Patch Changes

  • Fixed custom suspended-tool resume data and reset approval state when legacy stream approvals fail. (#21658)

@mastra/redis@1.4.1

Patch Changes

  • Fixed increment() never applying the configured TTL. Counter keys written via increment() now expire like every other key in RedisServerCache, preventing unbounded Redis key growth. (#21519)

@mastra/redis-streams@0.4.0

Minor Changes

  • Added live-tail Redis Streams subscriptions with startFrom: "latest". New consumer groups skip retained entries while existing groups keep their checkpoint. Subscriptions also preserve their position when Redis recreates a missing group. (#21535)

    await pubsub.subscribe(topic, callback, { startFrom: 'latest' });

Patch Changes

  • Fixed subscribe() creating Redis streams without a TTL. When streamIdleTtlMs is configured, the stream key created via MKSTREAM is now stamped with the TTL atomically, so topics that are subscribed to but never published to (for example per-run control topics) no longer linger in Redis forever. (#21519)

@mastra/server@1.60.0

Minor Changes

  • Added support for loading submitted agent plan Markdown from an agent workspace. (#21658)

Patch Changes

  • Fixed GET /agent-controller/:controllerId/sessions/:resourceId/threads and GET /agent-controller/:controllerId/sessions/:resourceId/threads/:threadId/messages provisioning a workspace/sandbox on every request. Both endpoints previously routed through session creation as a side effect, stalling read-only page visits 5–17s and consuming a sandbox slot per visit. They now read from storage directly. Session creation and workspace/sandbox provisioning continue to happen on the write path as before. (#21474)

  • Fixed serialized tool schemas to hide runtime-only background and suspended-run fields from user input forms. (#21451)

  • Fixed two things the agent controller's HTTP routes got wrong. (#21739)

    Threads listed through GET /agent-controller/:id/sessions/:resourceId/threads leaked internal session bookkeeping as scoping tags: a session's persisted thinkingLevel and notifications preferences showed up next to real tags like projectPath, and could be passed to the tags filter. They are now filtered out like the other reserved keys.

    Workspace failures streamed over SSE arrived empty. workspace_error and workspace_status_changed carry an Error, whose name and message are non-enumerable, so JSON serialization sent "error": {} and browser clients could not show why the workspace failed. Only the generic error event was being flattened; every event that carries an Error now is.

    The session-state route also returns a typed tokenUsage object instead of an opaque record, so clients get the same shape the display_state_changed event carries.

  • Fixed trace detail responses to report derived span statuses consistently with trace lists. (#21450)

  • Resolve the public origin from X-Mastra-Public-Host when present, ahead of X-Forwarded-Host. (#21787)

    Gateways that overwrite X-Forwarded-Host with their own internal domain (Railway's does) left getPublicOrigin() resolving an internal hostname, so deployed apps built OAuth callback URIs like https://my-project-qa-qa.up.railway.app/api/auth/sso/callback and sign-in failed against providers that validate the redirect URI. The same mis-resolved origin also failed the same-origin check on redirect_uri, silently collapsing the post-login landing page to /.

  • Fixed agent controller session state hydration to return durable tasks for the requested thread. (#21545)

@mastra/vectorize@1.1.1

Patch Changes

  • Fixed Cloudflare Vectorize updates inserting a duplicate vector under a random ID instead of replacing the requested one, and added support for deleting multiple vectors by ID. Deleting by metadata filter remains unsupported. (#21582) (#21694)

Other updated packages

The following packages were updated with dependency changes only: