mastra-ai/mastra · error

Factory binding ${binding.id} has no authenticated session o

Error message

Factory binding ${binding.id} has no authenticated session owner.

What it means

After finding an active binding, the dispatcher fetches the work item and reads the session's startedBy user for the binding's role. Wake runs must execute under the authenticated user who started the session (credentials are primed and a RequestContext user is set), so if startedBy is missing the library cannot attribute or authorize the run and throws immediately. This is a plain Error, recorded on the pending start and retried with backoff.

Source

Thrown at mastracode/factory/src/rules/dispatcher.ts:928

        async leaseExpiresAt =>
          this.#storage.renewPendingStartLease(leaseIdentity(record, this.#ownerId), leaseExpiresAt),
        async () => {
          if (record.message === null) return;
          const bindings = await this.#storage.listRunBindings(record.orgId, record.factoryProjectId);
          const binding = bindings.find(
            candidate => candidate.id === record.bindingId && candidate.status === 'active',
          );
          if (!binding) {
            throw new FactoryDispatchError(
              'session_unavailable',
              'Prepared Factory binding is unavailable or revoked.',
            );
          }
          // Wake runs build the Factory workspace, which requires the
          // authenticated session owner on the request context.
          const item = await this.#storage.get({ orgId: record.orgId, id: binding.workItemId });
          const startedBy = item?.sessions[binding.role]?.startedBy;
          if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);
          await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });
          const requestContext = new RequestContext();
          requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });
          const session = await this.#requireSession(binding);
          let resolveAgentEnd!: () => void;
          let agentEnd!: Promise<void>;
          // The run's own verdict, not the delivery's: a kickoff delivered
          // into a run that is already terminating is consumed without
          // execution, and completing the pending start on the delivery ack
          // alone strands the card with a success ledger entry.
          let endReason: 'complete' | 'aborted' | 'error' | 'suspended' | undefined;
          const armAgentEnd = () => {
            endReason = undefined;
            agentEnd = new Promise<void>(resolve => {
              resolveAgentEnd = resolve;
            });
          };
          armAgentEnd();

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Ensure the session for binding.role has startedBy populated — re-authenticate/record the starting user on the work item session.
  2. Check that binding.role matches an actual key in item.sessions (e.g. 'implement' vs 'review') and fix the binding's role if it was renamed.
  3. Backfill startedBy for imported/migrated work item rows before re-enabling dispatch.
  4. If the item is missing entirely (get returned undefined), re-create the work item for binding.workItemId.

Example fix

// before: session rows written without an owner
await storage.put({ id: itemId, sessions: { implement: { /* no startedBy */ } } });
// after: always record the authenticated starter
await storage.put({ id: itemId, sessions: { implement: { startedBy: user.workosId } } });
Defensive patterns

Strategy: validation

Validate before calling

const item = await storage.get({ orgId, id: binding.workItemId });
const startedBy = item?.sessions?.[binding.role]?.startedBy;
if (!startedBy) throw new Error(`work item ${binding.workItemId} lacks startedBy for role ${binding.role}`);

Type guard

function hasSessionOwner(binding, item) {
  return typeof item?.sessions?.[binding.role]?.startedBy === 'string' &&
    item.sessions[binding.role].startedBy.length > 0;
}

Try / catch

try {
  await dispatcher.dispatch(record);
} catch (e) {
  if (String(e?.message).includes('has no authenticated session owner')) {
    await backfillSessionOwner(record); // re-authenticate and persist startedBy
  } else throw e;
}

Prevention

When it happens

Trigger: The work item row exists but item.sessions[binding.role] is undefined, or that session exists without a startedBy field — typically sessions created outside the authenticated flow, partially-migrated rows, or a role mismatch between the binding and stored sessions.

Common situations: Sessions seeded by scripts/imports that omit startedBy; a binding whose role was renamed or re-pointed after the session map was written; older schema rows predating the startedBy field; tests that stub work items without session metadata.

Understand the failure class

Related errors


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