github mastra-ai/mastra @mastra/core@1.41.0
June 4, 2026

4 hours ago

Highlights

Per-request Workspace sandboxes (multi-tenant + isolation)

@mastra/core Workspace sandbox can now be a resolver function, enabling per-request sandbox routing for multi-tenant deployments with isolated working directories/permissions; includes prompt-safe “stable placeholder” instructions by default with an opt-in instructions.dynamicSandbox mode.

Sandbox continuity for background processes via sandboxCacheKey

When using dynamic sandboxes, sandboxCacheKey lets background process tools (execute_command with background: true, get_process_output, kill_process) reliably attach to the same sandbox across follow-up requests, with cache management via workspace.clearSandboxCache().

Unified “stream until idle” behavior via untilIdle option (core + server + client SDKs)

stream()/resumeStream() now support untilIdle (boolean or { maxIdleMs }) to keep streams open across background-task continuations; the same untilIdle request field is supported on server endpoints and client SDKs.

Deprecation: dedicated *UntilIdle methods/endpoints

streamUntilIdle() and resumeStreamUntilIdle() (and the dedicated /stream-until-idle and /resume-stream-until-idle endpoints) are deprecated in favor of stream(..., { untilIdle: true }) / resumeStream(..., { untilIdle: true }).

Breaking Changes

  • None noted in this changelog.

Changelog

@mastra/core@1.41.0

Minor Changes

  • Workspace sandbox now accepts a resolver function for per-request sandboxes. (#16048)

    Before: sandbox: WorkspaceSandbox (static, same sandbox for every request)
    After: sandbox: WorkspaceSandbox | (({ requestContext }) => WorkspaceSandbox) (static or per-request)

    This enables per-request sandbox routing from a single Workspace — useful for multi-tenant deployments where each user/role needs an isolated working directory or different execution permissions.

    const workspace = new Workspace({
      sandbox: ({ requestContext }) => {
        const userId = requestContext.get('user-id') as string;
        return new LocalSandbox({ workingDirectory: `/workspaces/${userId}` });
      },
    });

    When using a resolver, the caller owns the returned sandbox's lifecycle — the Workspace will not call start() or destroy() on it. mounts throws an INVALID_CONFIG error with a resolver, and lsp: true is disabled with a warning because both require a concrete sandbox instance up front.

    Stable prompts by default

    Building workspace instructions no longer calls a sandbox resolver. Resolver-backed sandboxes contribute stable placeholder text to the agent's system message, so constructing the prompt never provisions a caller-owned sandbox and the prompt stays cache-friendly. Opt into concrete per-request instructions with instructions.dynamicSandbox:

    const workspace = new Workspace({
      sandbox: ({ requestContext }) => resolveSandbox(requestContext),
      instructions: { dynamicSandbox: 'resolve' }, // or a ({ requestContext }) => string function
    });

    Background process continuity

    Set sandboxCacheKey to keep execute_command({ background: true }), get_process_output, and kill_process on the same sandbox across follow-up requests — continuity is keyed by a stable id rather than the RequestContext instance:

    const workspace = new Workspace({
      sandbox: ({ requestContext }) => resolveSandbox(requestContext),
      sandboxCacheKey: ({ requestContext }) => requestContext.get('thread-id') as string,
    });

    Failed sandbox resolver calls are removed from the cache so later calls can retry. Use workspace.clearSandboxCache(cacheKey) to drop a keyed sandbox reference when your own lifecycle code has destroyed or replaced that sandbox.

    When background process tools cannot find a PID on a dynamic sandbox without sandboxCacheKey, the tool output now points to sandboxCacheKey so callers can fix continuity across follow-up requests.

Patch Changes

  • Added untilIdle option to stream() and resumeStream() methods. Pass untilIdle: true (or untilIdle: { maxIdleMs: 60_000 }) to keep the stream open across background-task continuations — same behavior as the now-deprecated streamUntilIdle() method. (#17536)

    Example:

    const result = await agent.stream('Research solana for me', {
      untilIdle: true,
      memory: { thread: 't1', resource: 'u1' },
    });

    Deprecated streamUntilIdle() and resumeStreamUntilIdle() — they still work but now delegate internally to stream({ untilIdle: true }).

@mastra/acp@0.2.1

Patch Changes

  • Fixed ACP tools to keep their default session alive across executions. (#17516)

@mastra/client-js@1.23.2

Patch Changes

  • The /agents/:agentId/stream and /agents/:agentId/resume-stream endpoints now accept an untilIdle field in the request body. When set, the stream stays open across background-task continuations (same behavior as the /stream-until-idle endpoint). The dedicated /stream-until-idle and /resume-stream-until-idle endpoints remain available but are deprecated. (#17536)

    Server example:

    // POST /api/agents/:agentId/stream
    fetch(`/api/agents/${agentId}/stream`, {
      method: 'POST',
      body: JSON.stringify({
        messages: [{ role: 'user', content: 'Research solana for me' }],
        untilIdle: true, // or { maxIdleMs: 60000 }
      }),
    });

    Client SDK: streamUntilIdle() and resumeStreamUntilIdle() are deprecated — use stream(messages, { untilIdle: true }) instead.

@mastra/playground-ui@32.0.2

Patch Changes

  • Fixed dropdown, combobox, select, and popover popups silently failing to render when opened outside a side panel (most visibly the model and provider pickers in the chat composer). The shared portal-container resolver now always falls back to the document body instead of leaking an unrenderable value. (#17560)

  • Fixed combobox popups so model picker dropdowns open correctly outside side dialogs. (#17556)

@mastra/react@0.5.2

Patch Changes

  • The /agents/:agentId/stream and /agents/:agentId/resume-stream endpoints now accept an untilIdle field in the request body. When set, the stream stays open across background-task continuations (same behavior as the /stream-until-idle endpoint). The dedicated /stream-until-idle and /resume-stream-until-idle endpoints remain available but are deprecated. (#17536)

    Server example:

    // POST /api/agents/:agentId/stream
    fetch(`/api/agents/${agentId}/stream`, {
      method: 'POST',
      body: JSON.stringify({
        messages: [{ role: 'user', content: 'Research solana for me' }],
        untilIdle: true, // or { maxIdleMs: 60000 }
      }),
    });

    Client SDK: streamUntilIdle() and resumeStreamUntilIdle() are deprecated — use stream(messages, { untilIdle: true }) instead.

@mastra/server@1.41.0

Minor Changes

  • The /agents/:agentId/stream and /agents/:agentId/resume-stream endpoints now accept an untilIdle field in the request body. When set, the stream stays open across background-task continuations (same behavior as the /stream-until-idle endpoint). The dedicated /stream-until-idle and /resume-stream-until-idle endpoints remain available but are deprecated. (#17536)

    Server example:

    // POST /api/agents/:agentId/stream
    fetch(`/api/agents/${agentId}/stream`, {
      method: 'POST',
      body: JSON.stringify({
        messages: [{ role: 'user', content: 'Research solana for me' }],
        untilIdle: true, // or { maxIdleMs: 60000 }
      }),
    });

    Client SDK: streamUntilIdle() and resumeStreamUntilIdle() are deprecated — use stream(messages, { untilIdle: true }) instead.

Patch Changes

Other updated packages

The following packages were updated with dependency changes only:

Don't miss a new mastra release

NewReleases is sending notifications on new releases.