mastra-ai/mastra · error

Pin limit reached: the set holds at most ${options.maxPins}.

Error message

Pin limit reached: the set holds at most ${options.maxPins}. Unpin something first.

What it means

Pin writes enforce a budget via assertBudget before appending to the reserved pinned-knowledge node. The set may hold at most options.maxPins pins; when adding a NEW pin (no replacing record) would meet or exceed that count, the tool throws. This keeps the always-injected pin set small and predictable. Replacing an existing pin bypasses the count check because the set size does not grow.

Source

Thrown at packages/memory/src/processors/observational-memory/subconscious/pinned.ts:115

    pins.push(...page.records);
    after = page.nextCursor;
  } while (after);
  return { nodeId, pins };
}

function totalCharacters(pins: KnowledgeRecord[]): number {
  return pins.reduce((sum, pin) => sum + pin.text.length, 0);
}

function assertBudget(
  options: PinnedToolsOptions,
  pins: KnowledgeRecord[],
  incomingText: string,
  replacing?: KnowledgeRecord,
): void {
  const kept = replacing ? pins.filter(pin => pin.id !== replacing.id) : pins;
  if (!replacing && kept.length >= options.maxPins) {
    throw new Error(`Pin limit reached: the set holds at most ${options.maxPins}. Unpin something first.`);
  }
  if (totalCharacters(kept) + incomingText.length > options.maxCharacters) {
    throw new Error(`Pin budget exceeded: the pin set is limited to ${options.maxCharacters} characters in total.`);
  }
}

// Pins cannot be written broader than the resource level: the reserved node
// is anchored at (or below) the resource, and an org-scoped pin would only be
// resolvable from the resource that created it, which is a silent-loss trap.
function clampPinLevel(level: KnowledgeScopeLevel): KnowledgeScopeLevel {
  return level === 'org' ? 'resource' : level;
}

function resolveWriteScope(options: PinnedToolsOptions, level?: KnowledgeScopeLevel): KnowledgeScope {
  // An unscoped pin under a thread ceiling narrows to the ceiling instead of
  // failing the assert on every call: pins are model-driven, so a config that
  // makes the default request throw would be a tool error every turn.
  let effective = clampPinLevel(level ?? options.defaultScope);

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Unpin an existing record first, then pin the new one
  2. Replace an existing pin instead of adding (pass the record being replaced so it is filtered out of the count)
  3. Raise options.maxPins in the pinned tools configuration
  4. Audit the pin set (list pins) and prune stale entries

Example fix

// before: appending when already at cap
await pinTool.execute({ text: 'new fact' });
// Error: Pin limit reached: the set holds at most 10. Unpin something first.
// after: replace the least useful pin
await pinTool.execute({ text: 'new fact', replaceId: oldestPin.id });
Defensive patterns

Strategy: validation

Validate before calling

const pins = await listPins(memory);
if (pins.length >= maxPins) {
  throw new Error('Pin set is at capacity; unpin or replace before pinning new content.');
}

Type guard

function canAddPin(pins, maxPins) {
  return Array.isArray(pins) && pins.length < maxPins;
}

Try / catch

try {
  await pinTool.execute({ text });
} catch (err) {
  if (err.message.startsWith('Pin limit reached')) {
    // unpin the least valuable pin or replace it, then retry
  } else throw err;
}

Prevention

When it happens

Trigger: Calling the pin tool when the current pin count (excluding any replaced pin) is already >= maxPins and the write is an append, not a replacement.

Common situations: Long-running resources accumulating pins until the cap; maxPins lowered in config after pins already exist; automated flows pinning without ever unpinning.

Related errors


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