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
- Ensure every change to the interest set — additions or removals — atomically increments the generation counter.
- On catching this error, read acceptedGeneration, set the client counter to that value, increment, and retry with the full current interest set.
- Never partially mutate interests without bumping the generation; treat (generation, keySet) as an immutable pair.
- 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
- Treat (generation, keySet) as an immutable pair — never mutate one without the other.
- Centralize all interest mutations through a single function that bumps generation atomically.
- Never partially apply a diff to the interest set without incrementing generation.
- After any mutation, sync the client's local generation from the server's acceptedGeneration.
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
- invalid_generation
- stale_generation
- automation proposal not found
- brand backing project not found: ${projectId}
- unsupported brand asset protocol: ${parsed.protocol}
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/5c81c9418bf3f651.
Report an issue: GitHub.