nexu-io/open-design · warning · WorkspaceBillingInterestError

interest_capacity_exceeded

interest_capacity_exceeded

Error message

interest_capacity_exceeded

What it means

Thrown by setClientInterests when a single client's deduplicated interest set exceeds maxInterestsPerClient (default 16). The check occurs after normalizeKey/dedup via the Map at line 270-273, so duplicate keys count once. This cap prevents one client from monopolizing the coordinator's per-key refresh budget.

Source

Thrown at apps/daemon/src/collab/workspace-billing-runtime.ts:276

    const generationText = input.clientGeneration.trim();
    if (!clientId || !/^(?:0|[1-9]\d*)$/.test(generationText)) {
      throw new WorkspaceBillingInterestError('invalid_generation');
    }
    const generation = BigInt(generationText);
    const current = this.clients.get(clientId);
    if (current && generation < current.generation) {
      throw new WorkspaceBillingInterestError(
        'stale_generation',
        current.generation.toString(),
      );
    }
    const keys = new Map<string, WorkspaceBillingRuntimeKey>();
    for (const interest of input.interests) {
      const key = normalizeKey(interest);
      keys.set(runtimeKey(key), key);
    }
    if (keys.size > this.maxInterestsPerClient) {
      throw new WorkspaceBillingInterestError('interest_capacity_exceeded');
    }
    if (current && generation === current.generation) {
      if (!sameStringSet(current.keys, new Set(keys.keys()))) {
        throw new WorkspaceBillingInterestError(
          'generation_payload_mismatch',
          current.generation.toString(),
        );
      }
      current.expiresAt = this.scheduler.now() + this.interestLeaseMs;
      return this.interestLease(clientId, current);
    }
    if (keys.size === 0) {
      if (current) {
        this.clients.delete(clientId);
        this.handleInterestMutation(current.keys, new Set());
      }
      return {
        clientId,

View on GitHub (pinned to 5be4028344)

Solutions

  1. Reduce the interests array to at most maxInterestsPerClient entries, prioritizing the currently focused workspaces.
  2. Check the configured maxInterestsPerClient if you customized the coordinator options.
  3. Implement client-side eviction: drop least-recently-used interests when approaching the cap.
  4. If the cap is genuinely too low for your use case, raise maxInterestsPerClient in the coordinator constructor options.

Example fix

// before — sending all visible workspaces
const interests = allWorkspaces.map(w => ({ workspaceId: w.id, workspaceMemberId: w.memberId }));
runtime.setClientInterests({ clientId, clientGeneration, interests });

// after — cap to the focused set
const MAX = 16;
const interests = focusedWorkspaces
  .slice(0, MAX)
  .map(w => ({ workspaceId: w.id, workspaceMemberId: w.memberId }));
runtime.setClientInterests({ clientId, clientGeneration, interests });
Defensive patterns

Strategy: validation

Validate before calling

const MAX_INTERESTS_PER_CLIENT = 16; // must match coordinator config

function sanitizeInterests(
  interests: WorkspaceBillingRuntimeKey[],
  max: number = MAX_INTERESTS_PER_CLIENT,
): WorkspaceBillingRuntimeKey[] {
  // Dedup by workspaceId+workspaceMemberId
  const seen = new Set<string>();
  const deduped = interests.filter(k => {
    const id = `${k.workspaceId}:${k.workspaceMemberId}`;
    if (seen.has(id)) return false;
    seen.add(id);
    return true;
  });
  if (deduped.length > max) {
    throw new Error(
      `Interest set of ${deduped.length} exceeds per-client cap of ${max}. ` +
      `Reduce to the most important workspaces.`
    );
  }
  return deduped;
}

// Before calling setClientInterests:
const safeInterests = sanitizeInterests(interests, coordinator.maxInterestsPerClient);

Try / catch

try {
  await coordinator.setClientInterests({ clientId, clientGeneration: String(gen), interests });
} catch (error) {
  if (error instanceof WorkspaceBillingInterestError &&
      error.code === 'interest_capacity_exceeded') {
    // Trim to the cap, prioritizing focused workspaces
    const trimmed = interests.slice(0, MAX_INTERESTS_PER_CLIENT);
    await coordinator.setClientInterests({ clientId, clientGeneration: String(gen), interests: trimmed });
  } else throw error;
}

Prevention

When it happens

Trigger: Calling setClientInterests with an interests array that, after key normalization and dedup, produces more than maxInterestsPerClient distinct {workspaceId, workspaceMemberId} pairs. The default cap is 16.

Common situations: A dashboard client tries to watch billing for all 30 workspaces it can see. A bug in client-side interest logic sends every workspace/member pair instead of the focused set. The client doesn't prune stale interests before adding new ones.

Related errors


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/1840d7619ecc72ab. Report an issue: GitHub.