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
- Verify the thread still exists and belongs to the same resource/workspace as the session before switching; recreate the thread if deleted
- Check the session.thread.switch implementation/version for silent-failure modes and surface an error from within switch
- Log session.thread.getId() and the intended threadId at the switch site to diagnose why the switch no-ops
- 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
- Confirm thread/resource ownership before calling thread.switch
- Keep emitEvent:false switch paths covered by tests asserting getId() afterwards
- Check for races: verify the thread exists immediately before switching
- Pin and test the session/thread API version used by the webhook path
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
- GitHub subscription ${subscription.id} is missing its sessio
- GitHub subscription ${subscription.id} has no Factory sessio
- GitHub integration is required to load webhook subscriptions
- GitHub integration is required to retire webhook subscriptio
- WorkOS webhook secret is required. Provide it in options or
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/f047998fa304299c.
Report an issue: GitHub.