paperclipai/paperclip · error · Error

Paperclip ingestion fan-out exceeds the hard cap of ${MAX_PA

Error message

Paperclip ingestion fan-out exceeds the hard cap of ${MAX_PAPERCLIP_DISTILLATION_FAN_OUT} enabled profiles.

What it means

Thrown by enableActiveProjectDistillation when the caller-supplied limit for simultaneously-distilled active projects exceeds MAX_PAPERCLIP_DISTILLATION_FAN_OUT (25). The cap exists because each enabled profile triggers LLM distillation work; an unbounded fan-out would explode cost and worker load. The check runs before any DB write so the request is rejected atomically.

Source

Thrown at packages/plugins/plugin-llm-wiki/src/wiki/core.ts:2483

  if (project.managedByPlugin?.pluginKey === PLUGIN_ID) return false;
  if (project.managedByPlugin?.resourceKey === WIKI_PROJECT_KEY) return false;
  return true;
}

function projectActivityTimestamp(project: Project): string {
  return isoString(project.updatedAt) ?? new Date().toISOString();
}

export async function enableActiveProjectDistillation(ctx: PluginContext, input: {
  companyId: string;
  wikiId?: string | null;
  spaceSlug?: string | null;
  limit?: number | null;
}): Promise<EnableActiveProjectDistillationResult> {
  const wikiId = normalizeWikiId(input.wikiId);
  const space = await requirePaperclipIngestionPolicy(ctx, { companyId: input.companyId, wikiId, spaceSlug: input.spaceSlug }, "candidate_search", { requireEnabledProfile: true });
  if (typeof input.limit === "number" && Number.isFinite(input.limit) && Math.floor(input.limit) > MAX_PAPERCLIP_DISTILLATION_FAN_OUT) {
    throw new Error(`Paperclip ingestion fan-out exceeds the hard cap of ${MAX_PAPERCLIP_DISTILLATION_FAN_OUT} enabled profiles.`);
  }
  const limit = normalizeLimit(input.limit ?? 3, 3, 25);
  const projects = await ctx.projects.list({ companyId: input.companyId, limit: 200 });
  const activeProjects = projects
    .filter(isActiveDistillationProject)
    .sort((a, b) => projectActivityTimestamp(b).localeCompare(projectActivityTimestamp(a)))
    .slice(0, limit);

  const selectedProjects: EnableActiveProjectDistillationResult["selectedProjects"] = [];
  for (const project of activeProjects) {
    const observedAt = projectActivityTimestamp(project);
    const cursorId = await upsertPaperclipDistillationCursor(ctx, {
      companyId: input.companyId,
      wikiId,
      spaceId: space.id,
      projectId: project.id,
      rootIssueId: null,
      observedAt,

View on GitHub (pinned to 67001ec6eb)

Solutions

  1. Set input.limit to a value between 1 and 25 (inclusive), or omit it to accept the default of 3 via normalizeLimit(input.limit ?? 3, 3, 25).
  2. If you genuinely need broader coverage, run enableActiveProjectDistillation multiple times with different project filters rather than raising the cap.
  3. If 25 is wrong for your deployment, change the MAX_PAPERCLIP_DISTILLATION_FAN_OUT constant in packages/plugins/plugin-llm-wiki/src/wiki/core.ts:33 and document the cost implications.

Example fix

// before
enableActiveProjectDistillation(ctx, { companyId, limit: 200 });
// after
enableActiveProjectDistillation(ctx, { companyId, limit: 25 });
// or omit limit entirely for the default of 3
Defensive patterns

Strategy: validation

Validate before calling

const MAX_FAN_OUT = 25;
function safeDistillationLimit(input: number | null | undefined): number | null {
  if (input == null) return null; // accept default of 3
  if (!Number.isFinite(input) || Math.floor(input) > MAX_FAN_OUT) {
    throw new Error(`limit must be a finite number <= ${MAX_FAN_OUT}`);
  }
  return Math.floor(input);
}

Type guard

function isValidFanOutLimit(value: unknown): value is number {
  return typeof value === 'number' && Number.isFinite(value) && Math.floor(value) >= 1 && Math.floor(value) <= 25;
}

Try / catch

try {
  await enableActiveProjectDistillation(ctx, { companyId, limit });
} catch (err) {
  if (err instanceof Error && /fan-out exceeds the hard cap/.test(err.message)) {
    // clamp and retry, or surface to operator
  } else throw err;
}

Prevention

When it happens

Trigger: Calling the plugin action enable-active-project-distillation (or its backing export) with input.limit set to a finite number greater than 25. The guard is Math.floor(input.limit) > 25, evaluated only when input.limit is a finite number.

Common situations: An operator bumps the distillation limit in a routine/issue param to 'catch up' on backlog; a UI slider or config default was raised above 25; passing limit: 200 (the list() fetch size) by mistake instead of the distillation limit.

Related errors


AI-assisted analysis of paperclipai/paperclip@67001ec6eb (2026-08-12). Data as JSON: /api/errors/5abb9c2a5b14daea. Report an issue: GitHub.