{"record":{"id":"3a580dacf01770da","repo":"mastra-ai/mastra","slug":"github-subscription-subscription-id-has-no-fact","errorCode":null,"errorMessage":"GitHub subscription ${subscription.id} has no Factory session ${sessionId} to run as.","messagePattern":"GitHub subscription (.+?) has no Factory session (.+?) to run as\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/integrations/github/webhook.ts","lineNumber":368,"sourceCode":"  // The thread row is the authoritative answer; the stored id is only a fallback.\n  const thread = await controller.queryThreadById({ threadId });\n  if (!thread) return undefined;\n  const ownerResourceId = thread.resourceId || resourceId;\n  const scope = subscription.sessionScope || undefined;\n  let session = await controller.getSessionByResource(ownerResourceId, scope);\n  if (!session) {\n    const tags = {\n      factoryProjectId: resourceId,\n      projectRepositoryId: subscription.data.projectRepositoryId,\n    };\n    // Creating the session resolves its workspace, which authorizes the caller\n    // against the Factory session row — no signed-in user, so run as its owner.\n    // The session is created under the resource that owns the thread, so the\n    // thread switch below resolves; the persisted Factory session is keyed by\n    // the subscription's session ID.\n    const sessionRow = await github?.sourceControlStorage.sessions.getBySessionId(sessionId);\n    if (!sessionRow) {\n      throw new Error(`GitHub subscription ${subscription.id} has no Factory session ${sessionId} to run as.`);\n    }\n    const requestContext = new RequestContext();\n    requestContext.set('user', { workosId: sessionRow.userId, organizationId: sessionRow.orgId });\n    session = await controller.createSession({\n      id: sessionId,\n      ownerId: sessionRow.userId,\n      resourceId: ownerResourceId,\n      scope,\n      tags,\n      requestContext,\n    });\n    await seedSessionOrg(session, sessionRow.orgId);\n  } else if (!hasResolvedOrg(session.state?.get()?.factoryOrgId)) {\n    // A session created before the org seed existed carries the project tag and\n    // no org, so capture would refuse for the rest of its life even though the\n    // org is recoverable. Heal it — but only here, and only when it is missing:\n    // hoisting the row fetch above would make an existing-session delivery throw\n    // on a missing row where it previously succeeded, and fetching it every time","sourceCodeStart":350,"sourceCodeEnd":386,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/integrations/github/webhook.ts#L350-L386","documentation":"After verifying the subscription's binding fields, resolveSubscriptionSession looks up the persisted Factory session row by sessionId in sourceControlStorage. If no session row exists for that ID, there is no owner (userId/orgId) to run the webhook-triggered work as, so the code throws identifying both the subscription and the missing session. This prevents running webhook events against a fabricated or orphaned session identity.","triggerScenarios":"A GitHub PR webhook dispatch resolves a subscription whose sessionId no longer (or never did) exists in github.sourceControlStorage.sessions — getBySessionId returns undefined.","commonSituations":"The Factory session was deleted (user removed session / retention cleanup) but its subscriptions were not retired; the subscription was copied between environments so the session ID points nowhere; the storage backend differs between the writer and the webhook reader; a race where session creation failed after subscription persist.","solutions":["Retire the orphaned subscription (retirePullRequestSubscription) so future webhook events stop referencing the missing session","Re-create the Factory session with the same sessionId or re-create the subscription from an existing session","Check session retention/cleanup jobs to ensure they retire dependent subscriptions when deleting sessions","Wrap resolveSubscriptionSession in try-catch inside the webhook dispatcher to skip and log rather than failing the whole delivery"],"exampleFix":"// before\nconst sessionRow = await github?.sourceControlStorage.sessions.getBySessionId(sessionId);\nif (!sessionRow) throw new Error(`...no Factory session ${sessionId}...`);\n// after\nconst sessionRow = await github?.sourceControlStorage.sessions.getBySessionId(sessionId);\nif (!sessionRow) {\n  logger.warn('Subscription %s points to deleted session %s — retiring', subscription.id, sessionId);\n  await retirePullRequestSubscription(subscription.id, 'closed', github.integrationStorage);\n  return null;\n}","handlingStrategy":"try-catch","validationCode":"const sessionRow = await github?.sourceControlStorage.sessions.getBySessionId(subscription.sessionId);\nif (!sessionRow) {\n  logger.warn('Subscription %s references deleted session %s', subscription.id, subscription.sessionId);\n  await retirePullRequestSubscription(subscription.id, 'closed', github.integrationStorage);\n  return null;\n}","typeGuard":null,"tryCatchPattern":"try {\n  const session = await resolveSubscriptionSession(controller, subscription, github);\n} catch (err) {\n  if ((err as Error).message.includes('has no Factory session')) {\n    await retireSubscription(subscription.id, 'closed');\n    return;\n  }\n  throw err;\n}","preventionTips":["Retire dependent subscriptions whenever a Factory session is deleted","Avoid copying subscription rows across environments (session IDs are not portable)","Use a single storage backend for session reads on both subscription creation and webhook dispatch paths","Alert on subscriptions whose sessions fail lookup more than once"],"tags":["github","webhook","orphaned-session"],"backgroundTag":"orphaned-session-reference","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}