{"record":{"id":"55cb3fab1791aa25","repo":"nexu-io/open-design","slug":"stale-generation","errorCode":"stale_generation","errorMessage":"stale_generation","messagePattern":"stale_generation","errorType":"exception","errorClass":"WorkspaceBillingInterestError","httpStatus":null,"severity":"warning","filePath":"apps/daemon/src/collab/workspace-billing-runtime.ts","lineNumber":265,"sourceCode":"    this.interestSweepTimer = this.scheduler.setInterval(() => {\n      this.sweepExpiredInterests();\n    }, Math.max(1, options.interestSweepIntervalMs ?? DEFAULT_INTEREST_SWEEP_INTERVAL_MS));\n    this.interestSweepTimer.unref?.();\n  }\n\n  setClientInterests(\n    input: WorkspaceBillingRuntimeInterestSet,\n  ): WorkspaceBillingRuntimeInterestLease {\n    this.assertUsable();\n    const clientId = input.clientId.trim();\n    const generationText = input.clientGeneration.trim();\n    if (!clientId || !/^(?:0|[1-9]\\d*)$/.test(generationText)) {\n      throw new WorkspaceBillingInterestError('invalid_generation');\n    }\n    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        );","sourceCodeStart":247,"sourceCodeEnd":283,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/collab/workspace-billing-runtime.ts#L247-L283","documentation":"Thrown by setClientInterests when the client sends a generation number lower than the one the coordinator already has tracked for that clientId. This is an optimistic concurrency guard: each client must monotonically increase its generation with every interest-set mutation. A stale generation indicates the client's local state is behind — likely because a previous mutation succeeded server-side but the client didn't record the new generation (e.g., a race between two concurrent calls from the same client). The error includes the server's current accepted generation as the second argument so the client can re-sync.","triggerScenarios":"Client A sends generation 3, which succeeds. Client A's local counter is still at 2 (didn't process the ack) and sends generation 2 again. Or two concurrent requests from the same clientId race: the second commit (generation 3) lands first, then the first commit (generation 2) arrives and is rejected as stale.","commonSituations":"Two browser tabs sharing the same clientId send interest updates concurrently. A reconnect after network loss replays a stale request from an old connection. The client increments its counter locally but the HTTP response is lost, so on retry it sends the same generation.","solutions":["Read the acceptedGeneration from the error and set the client-side counter to max(local, acceptedGeneration) before retrying.","Serialize interest-set mutations per clientId so concurrent calls cannot interleave generations.","On reconnect, always fetch the current accepted generation from the server before submitting new interests.","Use a single in-flight request guard: do not send a new interest set until the previous one's lease is confirmed."],"exampleFix":"// before — fire-and-forget with no generation sync\nclient.sendInterests(generation++, keys);\n\n// after — reconcile on stale_generation\ntry {\n  const lease = await runtime.setClientInterests({ clientId, clientGeneration: String(gen), interests });\n  gen = BigInt(lease.acceptedGeneration);\n} catch (error) {\n  if (error instanceof WorkspaceBillingInterestError && error.code === 'stale_generation') {\n    gen = BigInt(error.acceptedGeneration!) + 1n;\n    await runtime.setClientInterests({ clientId, clientGeneration: String(gen), interests });\n  } else throw error;\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"export function isStaleGenerationError(\n  error: unknown,\n): error is WorkspaceBillingInterestError {\n  return (\n    error instanceof WorkspaceBillingInterestError &&\n    error.code === 'stale_generation'\n  );\n}","tryCatchPattern":"try {\n  const lease = await coordinator.setClientInterests({\n    clientId,\n    clientGeneration: String(generation),\n    interests,\n  });\n} catch (error) {\n  if (error instanceof WorkspaceBillingInterestError &&\n      error.code === 'stale_generation' &&\n      error.acceptedGeneration) {\n    // Reconcile: adopt the server's generation, bump, and retry\n    generation = BigInt(error.acceptedGeneration) + 1n;\n    const lease = await coordinator.setClientInterests({\n      clientId,\n      clientGeneration: String(generation),\n      interests,\n    });\n  } else throw error;\n}","preventionTips":["Always update the client-side generation from the lease's acceptedGeneration after each successful call.","Serialize interest-set mutations per clientId to prevent concurrent generation races.","On reconnect, fetch the server's current generation before submitting new interests.","Never reuse a generation number after it has been acknowledged by the server."],"tags":["billing","concurrency","optimistic-locking","retry"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}