mastra-ai/mastra · error · Error
GitHub subscription ${subscription.id} is missing its sessio
Error message
GitHub subscription ${subscription.id} is missing its session binding. What it means
resolveSubscriptionSession maps an incoming GitHub webhook-triggered signal subscription back to its Factory session. Every subscription row must carry sessionId, resourceId, and threadId; any missing field means the subscription was persisted incompletely and cannot be resolved to a runnable session. The code throws with the subscription ID so the broken row can be located and repaired.
Source
Thrown at mastracode/factory/src/integrations/github/webhook.ts:337
terminal,
metadata: {
...metadata,
pullRequestNumber: metadata.pullRequestNumber,
repositoryId: metadata.repositoryId,
installationId: metadata.installationId,
},
payload,
};
}
async function resolveSubscriptionSession(
controller: MountedMastraCode['controller'],
subscription: GithubSignalSubscriptionRow,
github?: GithubWebhookDispatchIntegration,
) {
const { sessionId, resourceId, threadId } = subscription;
if (!sessionId || !resourceId || !threadId) {
throw new Error(`GitHub subscription ${subscription.id} is missing its session binding.`);
}
// Read the thread straight from storage before touching sessions. This answers
// two questions at once, and `queryThreadById` does it without constructing a
// session (so no workspace or sandbox is provisioned just to make the check).
//
// First: do we even have this thread? A pull request's events can reach a
// deployment that never owned the subscribed thread, and delivery must not
// fabricate a session for a thread that lives somewhere else.
//
// Second: which resource owns it? The subscription records the Factory project
// as its `resourceId`, but an unscoped session is registered under its own id,
// so the stored value routinely names a resource that does not own the thread.
// 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);View on GitHub (pinned to 75dd419e61)
Solutions
- Inspect the subscription row by ID in integration storage and fill in the missing sessionId/resourceId/threadId, or retire the broken subscription
- Re-create the subscription from a healthy Factory session so all three binding fields persist
- Add a NOT NULL / validation constraint or creation-path check so subscriptions are never written without a full session binding
- In the webhook dispatcher, catch this error per-subscription and skip/log it instead of failing the whole delivery
Example fix
// before
const { sessionId, resourceId, threadId } = subscription;
if (!sessionId || !resourceId || !threadId) throw new Error(`...missing its session binding.`);
// after (defensive dispatch)
try {
const session = await resolveSubscriptionSession(controller, subscription, github);
} catch (err) {
logger.warn('Skipping subscription %s: %s', subscription.id, (err as Error).message);
await retireSubscription(subscription.id, status);
return null;
} Defensive patterns
Strategy: validation
Validate before calling
function subscriptionIsFullyBound(s: GithubSignalSubscriptionRow): boolean {
return Boolean(s.sessionId && s.resourceId && s.threadId);
}
if (!subscriptionIsFullyBound(subscription)) {
logger.warn('Skipping unbound subscription', { id: subscription.id });
return null;
} Type guard
function hasSessionBinding(s: GithubSignalSubscriptionRow): s is GithubSignalSubscriptionRow &
{ sessionId: string; resourceId: string; threadId: string } {
return typeof s.sessionId === 'string' && s.sessionId.length > 0 &&
typeof s.resourceId === 'string' && s.resourceId.length > 0 &&
typeof s.threadId === 'string' && s.threadId.length > 0;
} Try / catch
try {
const session = await resolveSubscriptionSession(controller, subscription, github);
} catch (err) {
if ((err as Error).message.includes('missing its session binding')) {
await retireSubscription(subscription.id, 'closed');
logger.warn('Retired subscription with missing binding', { id: subscription.id });
return;
}
throw err;
} Prevention
- Add NOT NULL constraints on sessionId/resourceId/threadId in the subscription table
- Validate full binding before persisting new subscriptions
- Run a migration backfill to retire or repair legacy rows missing bindings
- Catch per-subscription resolution errors in the webhook dispatcher so one bad row doesn't block delivery
When it happens
Trigger: A GitHub PR webhook fires and dispatchGithubWebhook resolves a subscription row (from listPullRequestSubscriptionsForWebhook) whose sessionId, resourceId, or threadId column is null/empty when resolveSubscriptionSession runs.
Common situations: A subscription was created before the session-binding columns were added (schema migration gap); the session that created the subscription crashed mid-persist; manual DB edits or a partial write left the row incomplete; subscription created via an older code path that did not capture threadId.
Related errors
- GitHub installation id is invalid.
- GitHub subscription ${subscription.id} has no Factory sessio
- Session ${sessionId} did not bind thread ${threadId}.
- GitHub integration is required to load webhook subscriptions
- GitHub integration is required to retire webhook subscriptio
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/b1e523d9067710ea.
Report an issue: GitHub.