nexu-io/open-design · error · WorkspaceBillingInterestError

generation_payload_mismatch

generation_payload_mismatch

Error message

generation_payload_mismatch

What it means

Thrown by setClientInterests when the client sends the exact same generation number it already has registered, but the key set differs from what was registered at that generation. This means the client is violating the protocol contract: a generation number must uniquely identify a complete interest set. If the set changed, the generation must have increased. The error includes the accepted generation so the client can reconcile.

Source

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

    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,
        acceptedGeneration: generation.toString(),
        leaseExpiresAt: new Date(this.scheduler.now()).toISOString(),
      };
    }

View on GitHub (pinned to 5be4028344)

Solutions

  1. Ensure every change to the interest set — additions or removals — atomically increments the generation counter.
  2. On catching this error, read acceptedGeneration, set the client counter to that value, increment, and retry with the full current interest set.
  3. Never partially mutate interests without bumping the generation; treat (generation, keySet) as an immutable pair.
  4. Audit client-side interest management for code paths that mutate keys without going through a single generation-managing function.

Example fix

// before — mutating interests without bumping generation
interests.add(newKey);
sendInterests(currentGeneration, [...interests]); // bug: same generation, new payload

// after — bump generation on every mutation
function commitInterests(newKeys: Key[]) {
  currentGeneration += 1n;
  interests = new Set(newKeys);
  sendInterests(currentGeneration.toString(), [...interests]);
}
Defensive patterns

Strategy: retry

Type guard

export function isGenerationPayloadMismatch(
  error: unknown,
): error is WorkspaceBillingInterestError {
  return (
    error instanceof WorkspaceBillingInterestError &&
    error.code === 'generation_payload_mismatch'
  );
}

Try / catch

try {
  await coordinator.setClientInterests({
    clientId,
    clientGeneration: String(generation),
    interests,
  });
} catch (error) {
  if (error instanceof WorkspaceBillingInterestError &&
      error.code === 'generation_payload_mismatch' &&
      error.acceptedGeneration) {
    // The server has a different key set at this generation.
    // Adopt the server's generation, increment, and resend the full current set.
    generation = BigInt(error.acceptedGeneration) + 1n;
    await coordinator.setClientInterests({
      clientId,
      clientGeneration: String(generation),
      interests, // the full, correct current set
    });
  } else throw error;
}

Prevention

When it happens

Trigger: Client sends generation 5 with keys [A, B], succeeds. Client then sends generation 5 again but with keys [A, C] — the generation is the same but the payload changed. This trips the sameStringSet check at line 281.

Common situations: Client-side bug where the generation counter is incremented conditionally or the interest mutation doesn't trigger a generation bump. A race condition where two code paths mutate the interest set but share a stale generation snapshot. The client applies a local diff to its interest set but forgets to bump the generation.

Related errors


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