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

  1. Inspect the subscription row by ID in integration storage and fill in the missing sessionId/resourceId/threadId, or retire the broken subscription
  2. Re-create the subscription from a healthy Factory session so all three binding fields persist
  3. Add a NOT NULL / validation constraint or creation-path check so subscriptions are never written without a full session binding
  4. 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

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


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/b1e523d9067710ea. Report an issue: GitHub.