{"record":{"id":"f047998fa304299c","repo":"mastra-ai/mastra","slug":"session-sessionid-did-not-bind-thread-threadi","errorCode":null,"errorMessage":"Session ${sessionId} did not bind thread ${threadId}.","messagePattern":"Session (.+?) did not bind thread (.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/integrations/github/webhook.ts","lineNumber":408,"sourceCode":"    try {\n      const sessionRow = await github?.sourceControlStorage.sessions.getBySessionId(sessionId);\n      await seedSessionOrg(session, sessionRow?.orgId);\n    } catch (error) {\n      console.warn('[GitHub webhook] Unable to resolve the session organization.', error);\n      await seedSessionOrg(session, undefined);\n    }\n  } else if (session.state?.get()?.factoryOrgUnresolved) {\n    // The org is present, so an earlier failed resolution left a stale marker\n    // behind. Clear it without a storage read — nothing else re-seeds a session\n    // once the start hook has run, so the marker would otherwise outlive its\n    // cause.\n    await seedSessionOrg(session, session.state.get()?.factoryOrgId);\n  }\n  if (session.thread.getId() !== threadId) {\n    await session.thread.switch({ threadId, emitEvent: false });\n  }\n  if (session.thread.getId() !== threadId) {\n    throw new Error(`Session ${sessionId} did not bind thread ${threadId}.`);\n  }\n  return session;\n}\n\n/**\n * Reviewer bots authorized out of the box. Deployments extend — never replace —\n * this set through the integration's `authorizedBots`.\n */\nexport const DEFAULT_AUTHORIZED_BOTS: readonly string[] = ['coderabbitai[bot]', 'devin-ai-integration[bot]'];\n\n/**\n * Parse a comma-separated `MASTRACODE_GITHUB_AUTHORIZED_BOTS` value into extra\n * bot logins. Returns undefined when nothing usable was configured.\n */\nexport function parseAuthorizedBotsEnv(value: string | undefined): string[] | undefined {\n  const bots = (value ?? '')\n    .split(',')\n    .map(bot => bot.trim())","sourceCodeStart":390,"sourceCodeEnd":426,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/integrations/github/webhook.ts#L390-L426","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait session.thread.switch({ threadId, emitEvent: false });\nif (session.thread.getId() !== threadId) {\n  throw new Error(`Session ${sessionId} did not bind thread ${threadId}.`);\n}\n// after\nconst threadRow = await storage.threads.getThreadById({ id: threadId });\nif (!threadRow) {\n  logger.warn('Thread %s missing; recreating binding for session %s', threadId, sessionId);\n  await recreateThreadBinding(session, threadId);\n}\nawait session.thread.switch({ threadId, emitEvent: false });\nif (session.thread.getId() !== threadId) {\n  await retirePullRequestSubscription(subscription.id, 'closed', github.integrationStorage);\n  throw new Error(`Session ${sessionId} did not bind thread ${threadId}.`);\n}","handlingStrategy":"validation","validationCode":"const currentThreadId = session.thread.getId();\nif (currentThreadId !== threadId) {\n  await session.thread.switch({ threadId, emitEvent: false });\n}\nif (session.thread.getId() !== threadId) {\n  throw new Error(`Aborting webhook handling: thread ${threadId} not bound to session ${sessionId}.`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await resolveSubscriptionSession(controller, subscription, github);\n} catch (err) {\n  if ((err as Error).message.includes('did not bind thread')) {\n    logger.error('Thread bind failed', { sessionId, threadId });\n    await recreateThreadBinding(sessionId, threadId);\n    return;\n  }\n  throw err;\n}","preventionTips":["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"],"tags":["github","webhook","thread-binding"],"backgroundTag":"thread-bind-failed","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}