mastra-ai/mastra · error · Error

Session ${sessionId} did not bind thread ${threadId}.

Error message

Session ${sessionId} did not bind thread ${threadId}.

What it means

After creating or loading the Factory session, resolveSubscriptionSession switches the session's active thread to the subscription's threadId and then re-verifies the switch took effect. If session.thread.getId() still differs from threadId after the switch call, the session is not actually bound to the thread the webhook targets, and proceeding would run GitHub events against the wrong conversation. The double-check catches silent switch failures.

Source

Thrown at mastracode/factory/src/integrations/github/webhook.ts:408

    try {
      const sessionRow = await github?.sourceControlStorage.sessions.getBySessionId(sessionId);
      await seedSessionOrg(session, sessionRow?.orgId);
    } catch (error) {
      console.warn('[GitHub webhook] Unable to resolve the session organization.', error);
      await seedSessionOrg(session, undefined);
    }
  } else if (session.state?.get()?.factoryOrgUnresolved) {
    // The org is present, so an earlier failed resolution left a stale marker
    // behind. Clear it without a storage read — nothing else re-seeds a session
    // once the start hook has run, so the marker would otherwise outlive its
    // cause.
    await seedSessionOrg(session, session.state.get()?.factoryOrgId);
  }
  if (session.thread.getId() !== threadId) {
    await session.thread.switch({ threadId, emitEvent: false });
  }
  if (session.thread.getId() !== threadId) {
    throw new Error(`Session ${sessionId} did not bind thread ${threadId}.`);
  }
  return session;
}

/**
 * Reviewer bots authorized out of the box. Deployments extend — never replace —
 * this set through the integration's `authorizedBots`.
 */
export const DEFAULT_AUTHORIZED_BOTS: readonly string[] = ['coderabbitai[bot]', 'devin-ai-integration[bot]'];

/**
 * Parse a comma-separated `MASTRACODE_GITHUB_AUTHORIZED_BOTS` value into extra
 * bot logins. Returns undefined when nothing usable was configured.
 */
export function parseAuthorizedBotsEnv(value: string | undefined): string[] | undefined {
  const bots = (value ?? '')
    .split(',')
    .map(bot => bot.trim())

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Verify the thread still exists and belongs to the same resource/workspace as the session before switching; recreate the thread if deleted
  2. Check the session.thread.switch implementation/version for silent-failure modes and surface an error from within switch
  3. Log session.thread.getId() and the intended threadId at the switch site to diagnose why the switch no-ops
  4. Treat this as a hard invariant in the dispatcher: catch the error, retire/recreate the subscription binding, and retry on the next webhook

Example fix

// before
await session.thread.switch({ threadId, emitEvent: false });
if (session.thread.getId() !== threadId) {
  throw new Error(`Session ${sessionId} did not bind thread ${threadId}.`);
}
// after
const threadRow = await storage.threads.getThreadById({ id: threadId });
if (!threadRow) {
  logger.warn('Thread %s missing; recreating binding for session %s', threadId, sessionId);
  await recreateThreadBinding(session, threadId);
}
await session.thread.switch({ threadId, emitEvent: false });
if (session.thread.getId() !== threadId) {
  await retirePullRequestSubscription(subscription.id, 'closed', github.integrationStorage);
  throw new Error(`Session ${sessionId} did not bind thread ${threadId}.`);
}
Defensive patterns

Strategy: validation

Validate before calling

const currentThreadId = session.thread.getId();
if (currentThreadId !== threadId) {
  await session.thread.switch({ threadId, emitEvent: false });
}
if (session.thread.getId() !== threadId) {
  throw new Error(`Aborting webhook handling: thread ${threadId} not bound to session ${sessionId}.`);
}

Try / catch

try {
  await resolveSubscriptionSession(controller, subscription, github);
} catch (err) {
  if ((err as Error).message.includes('did not bind thread')) {
    logger.error('Thread bind failed', { sessionId, threadId });
    await recreateThreadBinding(sessionId, threadId);
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: session.thread.switch({ threadId }) silently fails or is ignored (switch no-ops due to internal state, thread not visible to the session's workspace, or an implementation quirk), leaving session.thread.getId() !== threadId on re-check.

Common situations: The thread exists in storage but belongs to a different resource/workspace than the session, so switch refuses; a race where the thread was deleted between the storage pre-check and the switch; a version change in the thread-switch API that changed return/behavior; emitEvent:false path masking a failed switch.

Related errors


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