{"record":{"id":"5c81c9418bf3f651","repo":"nexu-io/open-design","slug":"generation-payload-mismatch","errorCode":"generation_payload_mismatch","errorMessage":"generation_payload_mismatch","messagePattern":"generation_payload_mismatch","errorType":"exception","errorClass":"WorkspaceBillingInterestError","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/collab/workspace-billing-runtime.ts","lineNumber":280,"sourceCode":"    const generation = BigInt(generationText);\n    const current = this.clients.get(clientId);\n    if (current && generation < current.generation) {\n      throw new WorkspaceBillingInterestError(\n        'stale_generation',\n        current.generation.toString(),\n      );\n    }\n    const keys = new Map<string, WorkspaceBillingRuntimeKey>();\n    for (const interest of input.interests) {\n      const key = normalizeKey(interest);\n      keys.set(runtimeKey(key), key);\n    }\n    if (keys.size > this.maxInterestsPerClient) {\n      throw new WorkspaceBillingInterestError('interest_capacity_exceeded');\n    }\n    if (current && generation === current.generation) {\n      if (!sameStringSet(current.keys, new Set(keys.keys()))) {\n        throw new WorkspaceBillingInterestError(\n          'generation_payload_mismatch',\n          current.generation.toString(),\n        );\n      }\n      current.expiresAt = this.scheduler.now() + this.interestLeaseMs;\n      return this.interestLease(clientId, current);\n    }\n    if (keys.size === 0) {\n      if (current) {\n        this.clients.delete(clientId);\n        this.handleInterestMutation(current.keys, new Set());\n      }\n      return {\n        clientId,\n        acceptedGeneration: generation.toString(),\n        leaseExpiresAt: new Date(this.scheduler.now()).toISOString(),\n      };\n    }","sourceCodeStart":262,"sourceCodeEnd":298,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/collab/workspace-billing-runtime.ts#L262-L298","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — mutating interests without bumping generation\ninterests.add(newKey);\nsendInterests(currentGeneration, [...interests]); // bug: same generation, new payload\n\n// after — bump generation on every mutation\nfunction commitInterests(newKeys: Key[]) {\n  currentGeneration += 1n;\n  interests = new Set(newKeys);\n  sendInterests(currentGeneration.toString(), [...interests]);\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"export function isGenerationPayloadMismatch(\n  error: unknown,\n): error is WorkspaceBillingInterestError {\n  return (\n    error instanceof WorkspaceBillingInterestError &&\n    error.code === 'generation_payload_mismatch'\n  );\n}","tryCatchPattern":"try {\n  await coordinator.setClientInterests({\n    clientId,\n    clientGeneration: String(generation),\n    interests,\n  });\n} catch (error) {\n  if (error instanceof WorkspaceBillingInterestError &&\n      error.code === 'generation_payload_mismatch' &&\n      error.acceptedGeneration) {\n    // The server has a different key set at this generation.\n    // Adopt the server's generation, increment, and resend the full current set.\n    generation = BigInt(error.acceptedGeneration) + 1n;\n    await coordinator.setClientInterests({\n      clientId,\n      clientGeneration: String(generation),\n      interests, // the full, correct current set\n    });\n  } else throw error;\n}","preventionTips":["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."],"tags":["billing","concurrency","protocol","data-integrity"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}