mastra-ai/mastra · error · Error
GitHub subscription ${subscription.id} has no Factory sessio
Error message
GitHub subscription ${subscription.id} has no Factory session ${sessionId} to run as. What it means
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.
Source
Thrown at mastracode/factory/src/integrations/github/webhook.ts:368
// The thread row is the authoritative answer; the stored id is only a fallback.
const thread = await controller.queryThreadById({ threadId });
if (!thread) return undefined;
const ownerResourceId = thread.resourceId || resourceId;
const scope = subscription.sessionScope || undefined;
let session = await controller.getSessionByResource(ownerResourceId, scope);
if (!session) {
const tags = {
factoryProjectId: resourceId,
projectRepositoryId: subscription.data.projectRepositoryId,
};
// Creating the session resolves its workspace, which authorizes the caller
// against the Factory session row — no signed-in user, so run as its owner.
// The session is created under the resource that owns the thread, so the
// thread switch below resolves; the persisted Factory session is keyed by
// the subscription's session ID.
const sessionRow = await github?.sourceControlStorage.sessions.getBySessionId(sessionId);
if (!sessionRow) {
throw new Error(`GitHub subscription ${subscription.id} has no Factory session ${sessionId} to run as.`);
}
const requestContext = new RequestContext();
requestContext.set('user', { workosId: sessionRow.userId, organizationId: sessionRow.orgId });
session = await controller.createSession({
id: sessionId,
ownerId: sessionRow.userId,
resourceId: ownerResourceId,
scope,
tags,
requestContext,
});
await seedSessionOrg(session, sessionRow.orgId);
} else if (!hasResolvedOrg(session.state?.get()?.factoryOrgId)) {
// A session created before the org seed existed carries the project tag and
// no org, so capture would refuse for the rest of its life even though the
// org is recoverable. Heal it — but only here, and only when it is missing:
// hoisting the row fetch above would make an existing-session delivery throw
// on a missing row where it previously succeeded, and fetching it every timeView on GitHub (pinned to 75dd419e61)
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
Example fix
// before
const sessionRow = await github?.sourceControlStorage.sessions.getBySessionId(sessionId);
if (!sessionRow) throw new Error(`...no Factory session ${sessionId}...`);
// after
const sessionRow = await github?.sourceControlStorage.sessions.getBySessionId(sessionId);
if (!sessionRow) {
logger.warn('Subscription %s points to deleted session %s — retiring', subscription.id, sessionId);
await retirePullRequestSubscription(subscription.id, 'closed', github.integrationStorage);
return null;
} Defensive patterns
Strategy: try-catch
Validate before calling
const sessionRow = await github?.sourceControlStorage.sessions.getBySessionId(subscription.sessionId);
if (!sessionRow) {
logger.warn('Subscription %s references deleted session %s', subscription.id, subscription.sessionId);
await retirePullRequestSubscription(subscription.id, 'closed', github.integrationStorage);
return null;
} Try / catch
try {
const session = await resolveSubscriptionSession(controller, subscription, github);
} catch (err) {
if ((err as Error).message.includes('has no Factory session')) {
await retireSubscription(subscription.id, 'closed');
return;
}
throw err;
} Prevention
- 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
When it happens
Trigger: A GitHub PR webhook dispatch resolves a subscription whose sessionId no longer (or never did) exists in github.sourceControlStorage.sessions — getBySessionId returns undefined.
Common situations: 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.
Related errors
- GitHub subscription ${subscription.id} is missing its sessio
- Session ${sessionId} did not bind thread ${threadId}.
- 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/3a580dacf01770da.
Report an issue: GitHub.