mastra-ai/mastra · error

createStagehand factory not set - required for thread scope

Error message

createStagehand factory not set - required for thread scope

What it means

In 'thread' scope, the StagehandThreadManager creates a dedicated Stagehand instance per thread by invoking an injected createStagehand factory. If that factory was never provided at construction, the manager cannot fulfill thread-scoped session creation and throws. This is a configuration/dependency-injection error, not a runtime browser problem.

Source

Thrown at browser/stagehand/src/thread-manager.ts:88

  }

  /**
   * Create a new session for a thread.
   */
  protected override async createSession(threadId: string): Promise<StagehandThreadSession> {
    // Check for saved browser state before creating new session (for browser restore)
    const savedState = this.getSavedBrowserState(threadId);

    const session: StagehandThreadSession = {
      threadId,
      createdAt: Date.now(),
      browserState: savedState,
    };

    if (this.scope === 'thread') {
      // Full thread scope - create a new Stagehand instance
      if (!this.createStagehand) {
        throw new Error('createStagehand factory not set - required for thread scope');
      }

      this.logger?.debug?.(`Creating dedicated Stagehand instance for thread ${threadId}`);
      const stagehand = await this.createStagehand();
      session.stagehand = stagehand;
      this.threadManagers.set(threadId, stagehand);

      // Restore browser state if available (before notifying parent to avoid screencast race)
      if (savedState && savedState.tabs.length > 0) {
        this.logger?.debug?.(`Restoring browser state for thread ${threadId}: ${savedState.tabs.length} tabs`);
        await this.restoreBrowserState(stagehand, savedState);
      }

      // Notify parent browser so it can set up close listeners
      // This is done after restoration so the screencast starts on the correct active page
      this.onBrowserCreated?.(stagehand, threadId);
    }
    // For 'shared' scope, no session setup needed - all threads share the instance

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Pass a createStagehand factory when constructing StagehandBrowser / the thread manager
  2. Ensure your constructor config actually enables thread scope intentionally and supplies the factory
  3. Update wiring code after library upgrades that made the factory required for thread scope
  4. Fall back to shared scope if per-thread instances are not needed

Example fix

// before
const browser = new StagehandBrowser({ scope: 'thread' });
// after
const browser = new StagehandBrowser({
  scope: 'thread',
  threadManagerOptions: {
    createStagehand: async () => new Stagehand({ env: 'LOCAL', ... }),
  },
});
Defensive patterns

Strategy: validation

Validate before calling

if (!threadManager.createStagehand) {
  throw new Error('thread scope requires a createStagehand factory at construction');
}

Type guard

function supportsThreadScope(m: { scope: string; createStagehand?: unknown }):
  m is { scope: 'thread'; createStagehand: () => Promise<Stagehand> } {
  return m.scope === 'thread' && typeof m.createStagehand === 'function';
}

Try / catch

try {
  await threadManager.createSession(threadId);
} catch (e) {
  if (e instanceof Error && e.message.includes('createStagehand factory not set')) {
    throw new Error('Misconfiguration: wire createStagehand when scope=thread');
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling createSession with scope='thread' on a StagehandBrowser whose thread manager was constructed without a createStagehand callback.

Common situations: Constructing the browser/thread manager manually without wiring the factory; copying setup code that assumed shared scope but switching to thread scope; library version upgrades requiring the factory for thread scope; DI container not providing the factory.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/efdb9b75a3710ff9. Report an issue: GitHub.