{"record":{"id":"96f7f70d1c18c76b","repo":"paperclipai/paperclip","slug":"a-reusable-lease-cannot-be-replaced-and-reacquired","errorCode":null,"errorMessage":"A reusable lease cannot be replaced and reacquired in the same operation.","messagePattern":"A reusable lease cannot be replaced and reacquired in the same operation\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"server/src/services/environments.ts","lineNumber":1400,"sourceCode":"        metadata: input.metadata ?? null,\n        createdAt: now,\n        updatedAt: now,\n      };\n      if (\n        (input.replacesReusableLeaseId || input.reusesReusableLeaseId) &&\n        (!input.executionWorkspaceId || !input.providerLeaseId)\n      ) {\n        throw new Error(\n          \"A reusable lease handoff requires an execution workspace and provider lease id.\",\n        );\n      }\n      if (input.reusesReusableLeaseId && !input.heartbeatRunId) {\n        throw new Error(\n          \"A same-run reusable lease reacquisition requires a heartbeat run id.\",\n        );\n      }\n      if (input.replacesReusableLeaseId && input.reusesReusableLeaseId) {\n        throw new Error(\n          \"A reusable lease cannot be replaced and reacquired in the same operation.\",\n        );\n      }\n      const row =\n        input.assertCompanyBinding ||\n        input.replacesReusableLeaseId ||\n        input.reusesReusableLeaseId\n          ? await db.transaction(async (tx) => {\n              if (input.assertCompanyBinding) {\n                // Lock the environment row first. Managed reconciliation locks the\n                // same sandbox environment rows with `for update` before it writes a\n                // company binding, so this lock serializes the two transactions on\n                // this row and closes the time-of-check to time-of-use window.\n                await tx\n                  .select({ id: environments.id })\n                  .from(environments)\n                  .where(eq(environments.id, input.environmentId))\n                  .for(\"update\");","sourceCodeStart":1382,"sourceCodeEnd":1418,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/server/src/services/environments.ts#L1382-L1418","documentation":"A lease operation may either replace a reusable lease with a new one (replacesReusableLeaseId) or reacquire the same lease (reusesReusableLeaseId), but not both at once. environmentService throws when both flags are set, because the two handoff semantics are mutually exclusive and the resulting state would be ambiguous.","triggerScenarios":"A single environmentService lease call passing both replacesReusableLeaseId and reusesReusableLeaseId — e.g. caller code that copies all prior lease fields into a new request, carrying over the old replace id while also adding reuse.","commonSituations":"Request builders that spread previous options into the next call; a retry path that re-sent the original replace flag while the run switched to reacquire semantics; confused migration code between the two lease modes.","solutions":["Decide the intended semantics and pass exactly one of replacesReusableLeaseId or reusesReusableLeaseId","Stop blindly spreading previous lease options into the new request; build options explicitly per operation","In retry logic, recompute the handoff mode instead of reusing stale flags","Add a caller-side assertion/validation to reject both-set inputs early"],"exampleFix":"// before\nupdateLease({ replacesReusableLeaseId: oldId, reusesReusableLeaseId: oldId })\n// after\nupdateLease({ reusesReusableLeaseId: oldId }) // reacquire OR replace, never both","handlingStrategy":"validation","validationCode":"if (input.replacesReusableLeaseId && input.reusesReusableLeaseId) {\n  throw new Error('choose either replace or reacquire, not both');\n}\nawait updateLease(input);","typeGuard":"function isExclusiveHandoff(i: {replacesReusableLeaseId?:string;reusesReusableLeaseId?:string}): boolean {\n  return !(i.replacesReusableLeaseId && i.reusesReusableLeaseId);\n}","tryCatchPattern":"try {\n  await environmentService.updateLease(input);\n} catch (e) {\n  if (e.message.includes('replaced and reacquired')) {\n    // Prefer reacquire when both were set with the same id\n    const { replacesReusableLeaseId, ...rest } = input;\n    return environmentService.updateLease({ ...rest, reusesReusableLeaseId: input.reusesReusableLeaseId ?? replacesReusableLeaseId });\n  }\n  throw e;\n}","preventionTips":["Build lease request options explicitly instead of spreading prior request objects","Model replace vs reacquire as a discriminated union type so both cannot be set at compile time","Zod schema with .refine() rejecting both-set inputs at the boundary","Review retry logic to recompute handoff mode rather than replaying stale flags"],"tags":["leases","validation","mutually-exclusive"],"backgroundTag":"mutually-exclusive-options","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}