Index: geniuswebcore/geniusweb/protocol/NegoSettings.py
===================================================================
--- geniuswebcore/geniusweb/protocol/NegoSettings.py	(revision 70)
+++ geniuswebcore/geniusweb/protocol/NegoSettings.py	(revision 71)
@@ -9,5 +9,6 @@
 
 @JsonSubTypes( ["geniusweb.protocol.session.saop.SAOPSettings.SAOPSettings",
-			"geniusweb.protocol.session.mopac.MOPACSettings.MOPACSettings" ])
+			"geniusweb.protocol.session.mopac.MOPACSettings.MOPACSettings",
+			"geniusweb.protocol.session.learn.LearnSettings.LearnSettings" ])
 @JsonTypeInfo(use = Id.NAME, include = As.WRAPPER_OBJECT)
 class NegoSettings(ABC):
Index: geniuswebcore/geniusweb/protocol/session/DefaultSessionState.py
===================================================================
--- geniuswebcore/geniusweb/protocol/session/DefaultSessionState.py	(revision 70)
+++ geniuswebcore/geniusweb/protocol/session/DefaultSessionState.py	(revision 71)
@@ -10,5 +10,5 @@
 from geniusweb.protocol.session.SessionState import SessionState
 from geniusweb.references.PartyWithProfile import PartyWithProfile
-from geniusweb.utils import toTuple
+from geniusweb.utils import toTuple, val
 
 
@@ -17,5 +17,5 @@
 
 
-class DefaultSessionState (SessionState, Generic[S,P]):
+class DefaultSessionState (SessionState, Generic[P,S]):
 	'''
 	The default current state of the session. immutable.
@@ -81,5 +81,5 @@
 		@param actions1       the new {@link Action}s
 		@param conns          the new connected {@link PartyId}s
-		@param progress1      the new {@link Progress}
+		@param progress1      the new {@link Progress}, can be None still
 		@param settings1      the new {@link SessionSettings}. Normally this is
 		                      constant during a session.
@@ -133,5 +133,5 @@
 
 	def WithDeadlineReached(self)->P :
-		return self.With(self._actions, self._connections, self._progress, self._settings, 
+		return self.With(self._actions, self._connections, val(self._progress), self._settings, 
 				self._partyprofiles, self._error) 
 
@@ -140,5 +140,5 @@
 		newconn = list(self._connections)
 		newconn.remove(party);
-		return self.With(self._actions, newconn, self._progress, \
+		return self.With(self._actions, newconn, val(self._progress), \
 				self._settings, self._partyprofiles, self._error)
 
@@ -149,5 +149,5 @@
 		'''
 		assert isinstance(e, ProtocolException)
-		return self.With(self._actions, self._connections, self._progress, 
+		return self.With(self._actions, self._connections, val(self._progress), 
 						self._settings, self._partyprofiles, e)
 
@@ -159,5 +159,5 @@
 		newprofiles = dict(self.getPartyProfiles())
 		newprofiles[connection]= partyprofile
-		return self.With(self.getActions(), newconns, self.getProgress(), self.getSettings(),
+		return self.With(self.getActions(), newconns, self._progress, self.getSettings(),
 				newprofiles, None)
 
@@ -189,5 +189,5 @@
 		newactions = list(self.getActions())
 		newactions.append(action)
-		return self.With(newactions, self._connections, self._progress, 
+		return self.With(newactions, self._connections, val(self._progress), 
 				self._settings, self._partyprofiles,self._error)
 
Index: geniuswebcore/geniusweb/protocol/session/learn/Learn.py
===================================================================
--- geniuswebcore/geniusweb/protocol/session/learn/Learn.py	(revision 71)
+++ geniuswebcore/geniusweb/protocol/session/learn/Learn.py	(revision 71)
@@ -0,0 +1,278 @@
+from datetime import datetime
+import logging
+from threading import Timer
+import threading
+from time import time, sleep
+from typing import Optional, List
+
+from tudelft.utilities.listener.DefaultListenable import DefaultListenable
+from tudelft.utilities.listener.Listener import Listener
+from tudelft.utilities.repository.NoResourcesNowException import NoResourcesNowException
+from tudelft_utilities_logging.Reporter import Reporter
+from uri.uri import URI
+
+from geniusweb.actions.Action import Action
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.events.ProtocolEvent import ProtocolEvent
+from geniusweb.inform.Finished import Finished
+from geniusweb.inform.Inform import Inform
+from geniusweb.inform.Settings import Settings
+from geniusweb.progress.ProgressFactory import ProgressFactory
+from geniusweb.protocol.CurrentNegoState import CurrentNegoState
+from geniusweb.protocol.NegoState import NegoState
+from geniusweb.protocol.ProtocolException import ProtocolException
+from geniusweb.protocol.partyconnection.ProtocolToPartyConn import ProtocolToPartyConn
+from geniusweb.protocol.partyconnection.ProtocolToPartyConnFactory import ProtocolToPartyConnFactory
+from geniusweb.protocol.partyconnection.ProtocolToPartyConnections import ProtocolToPartyConnections
+from geniusweb.protocol.session.SessionProtocol import SessionProtocol
+from geniusweb.protocol.session.learn.LearnState import LearnState
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+from geniusweb.references.ProtocolRef import ProtocolRef
+from geniusweb.utils import val
+
+
+MIN_SLEEP_TIME = 1000
+MAX_SLEEP_TIME = 60000
+TIME_MARGIN = 20 # ms extra delay after deadline
+LEARN = ProtocolRef(URI("Learn"))
+	 
+class Learn (DefaultListenable[ProtocolEvent],SessionProtocol):
+	'''
+	The Learn protocol allows parties to learn until the deadline set in the
+	{@link LearnSettings}.
+	'''
+	_deadlinetimer:Optional[Timer] = None
+# 	private volatile AtomicBoolean isFinishedInfoSent = new AtomicBoolean(
+# 			false);
+	_conns = ProtocolToPartyConnections([])
+	_synclock = threading.RLock()
+	_isFinishedInfoSent = False 
+
+	def __init__(self, state:LearnState, logger:Reporter ):
+		super().__init__()
+		self._state = state
+		self._log = logger
+
+	def start(self, connectionfactory:ProtocolToPartyConnFactory ):
+		try:
+			self._connect(connectionfactory)
+			self._setDeadline()
+			self._setupParties()
+		except Exception as e:
+			self._handleError("Failed to start up session", None, e)
+
+	def getDescription(self)->str:
+		return "Sends all parties the Settings. Parties can start learning immediately but must respect the deadline. When party is done, it should send LearningDone."
+
+	def getState(self)->NegoState :
+		return self._state
+
+
+	def getRef(self)-> ProtocolRef:
+		return LEARN
+
+	def addParticipant(self, party:PartyWithProfile ) :
+		raise ValueError(
+				"Dynamic joining a negotiation is not allowed in LEARN")
+
+	#####################################################################
+	# private functions. Some are protected only, for testing purposes
+	#####################################################################
+	def _connect(self, connectionfactory:ProtocolToPartyConnFactory ):
+		'''
+		step 1 in protocol: connect all involved parties and start the clock.
+		This always "succeeds" with a valid (but possibly final) state
+		<p>
+		This is 'protected' to allow junit testing, this code is not a 'public'
+		part of the interface.
+		
+		@param connectionfactory the connectionfactory for making party
+                			 connections
+		
+		@throws InterruptedException if the connection procedure is unterrupted
+		
+		@throws IOException  if this fails to properly conect to the
+                             parties, eg interrupted or server not
+                             responding..
+		'''
+		with self._synclock:
+			participants:List[PartyWithProfile] = self._state.getSettings().getAllParties()
+			
+			parties = [parti.getParty().getPartyRef() for parti in participants ]
+			connections:Optional[List[ProtocolToPartyConn]]  = None
+			self._log.log(logging.INFO, "LEARN connect " + str(parties))
+			while connections == None:
+				try:
+					connections = connectionfactory.connectAll(parties) # type:ignore
+				except NoResourcesNowException as e:
+					waitms = (e.getLater().timestamp()-time())*1000
+					self._log.log(logging.INFO,
+							"No resources available to run session, waiting"+ str(waitms))
+					sleep(min(MAX_SLEEP_TIME,
+							max(MIN_SLEEP_TIME, waitms))/1000)
+			# need val because mypy fails to see that connections can't be None here.
+			for i  in range(len(participants)):
+				self._conns = self._conns.With(val(connections)[i])
+				self._setState(self._state.WithParty(val(connections)[i].getParty(),
+						participants[i]))
+
+	def  _actionRequest(self, partyconn:ProtocolToPartyConn , action:Action ) :
+		'''
+		This is called when one of the party connections does an action.
+		Synchronized so that we always handle only 1 action at a time.
+		
+		@param partyconn the connection on which the action came in.
+		@param action    the {@link Action} taken by some party
+		'''	
+		with self._synclock:
+			if action == None:
+				err = partyconn.getError()
+				if err == None:
+					err = ProtocolException("Party sent a null action",
+							partyconn.getParty())
+				self._handleError(str(partyconn) + "Protocol error", partyconn.getParty(),
+						err) #type:ignore
+				return
+	
+			try:
+				self._setState(self._state.WithAction(partyconn.getParty(), action))
+			except Exception as  e:
+				self._handleError("failed to handle action " + str(action),
+						partyconn.getParty(), e)
+
+
+
+
+	def _setupParties(self):
+		'''
+		step 2 in protocol: listen to connections and send settings to the
+		parties.
+		<p>
+		This is 'protected' to allow junit testing, this code is not a 'public'
+		part of the interface.
+		
+		@throws ProtocolException if a party does not follow the protocol
+		'''
+		this=self
+		class MyListener(Listener[Action]):
+			def notifyChange(self, act:Action):
+				this._actionRequest(act)
+
+		
+		with self._synclock:
+
+			for conn in self._conns:
+				conn.addListener(MyListener())
+	
+			for connection in self._conns:
+				try:
+					self._sendSettings(connection)
+				except ConnectionError as e:
+					raise ProtocolException("Failed to initialize",
+							connection.getParty(), e)
+
+
+	def _sendSettings(self, connection:ProtocolToPartyConn ):
+		'''
+		Inform a party about its settings
+		
+		@param connection
+		@throws IOException if party got disconnected
+		'''
+		with self._synclock:
+			partyid = connection.getParty()
+			profile = self._state.getPartyProfiles()[partyid].getProfile()
+			params = self._state.getPartyProfiles()[partyid].getParty().getParameters()
+			if profile == None:
+				raise ValueError(
+						"Missing profile for party " + str(connection.getReference()))
+			connection.send(Settings(connection.getParty(), profile, self.getRef(),
+					val(self._state.getProgress()), params))
+
+	def _setDeadline(self):
+		'''
+		Set state to proper deadline. Starts the timer tasks. This tasks triggers
+		a call to handleError when the session times out.
+		'''
+		with self._synclock:
+			now = int(time()*1000)
+			deadline = self._state.getSettings().getDeadline()
+			self._setState(self._state.WithProgress(ProgressFactory.create(deadline, now)))
+			duration=(deadline.getDuration() + TIME_MARGIN)/1000.0
+			self._deadlinetimer = Timer(duration, self._timertask);
+
+			# set timer TIME_MARGIN after real deadline to ensure we're not too early
+			duration=(deadline.getDuration() + TIME_MARGIN)/1000.0
+			self._deadlinetimer = Timer( duration, self._timertask)
+			self._deadlinetimer.start()
+			self._log.log(logging.INFO, "SAOP deadline set to "
+					+ datetime.utcfromtimestamp( (duration+now)/1000. ).strftime('%Y/%m/%d %H:%M:%S'))
+
+	def _timertask(self):
+		if not self._state.isFinal(1000.*time()):
+			self._log.log(logging.CRITICAL,
+					"BUG. Deadline timer has triggered but state is not final")
+		self._log.log(logging.INFO,
+				"LEARN deadline reached. Terminating session.")
+		self._finish()
+
+
+	def _handleError(self, message:str, party:Optional[PartyId], e:Exception):
+		'''
+		Update state to include the given error and finishes up the session.
+		
+		@param message The message to attach to the error
+		@param party   the party where the error occured
+		@param e       the exception that occured.
+		'''
+		with self._synclock:
+			if isinstance(e,  ProtocolException):
+				self._setState(self._state.WithException( e))
+			else:
+				self._setState(self._state.WithException(ProtocolException(message, party, e)))
+			self._log.log(logging.WARNING, "LEARN protocol intercepted error due to party "\
+					+ str(party) + ":" + message, e)
+
+	def _setState(self, newstate:LearnState) :
+		'''
+		Sets the new state. If the new state is final, the finish-up procedure is
+		executed.
+		
+		@param newstate the new state.
+		'''
+		with self._synclock:
+			now = int(1000*time())
+			if self._state.isFinal(now):
+				self._finish()
+				return
+			self._state = newstate;
+			if newstate.isFinal(now):
+				self._finish()
+				
+	def _finish(self):
+		'''
+		Called when we reach final state. Cancels deadline timer. Send finished
+		info to all parties, notify current nego state as final and set
+		{@link #isFinishedInfoSent}. Double calls are automatically ignored.
+		'''
+		with self._synclock:
+			if self._deadlinetimer :
+				self._deadlinetimer.cancel()
+				self._deadlinetimer = None
+			if self._isFinishedInfoSent:
+				return;
+			self._isFinishedInfoSent=True
+			finished = Finished(self._state.getAgreements());
+			for conn in self._conns:
+				self._sendFinish(conn, finished)
+			self.notifyListeners(CurrentNegoState(self._state))
+
+	
+	def _sendFinish(self, connection:ProtocolToPartyConn , finished:Inform ):
+		try:
+			connection.send(finished)
+			connection.close()
+		except Exception as e:
+			self._log.log(logging.INFO, "Failed to send Finished to " + str(connection), e)
+
+
Index: geniuswebcore/geniusweb/protocol/session/learn/LearnSettings.py
===================================================================
--- geniuswebcore/geniusweb/protocol/session/learn/LearnSettings.py	(revision 71)
+++ geniuswebcore/geniusweb/protocol/session/learn/LearnSettings.py	(revision 71)
@@ -0,0 +1,121 @@
+from typing import List
+from uuid import UUID
+
+from tudelft_utilities_logging.Reporter import Reporter
+
+from geniusweb.deadline.Deadline import Deadline
+from geniusweb.protocol.session.SessionProtocol import SessionProtocol
+from geniusweb.protocol.session.SessionSettings import SessionSettings
+from geniusweb.protocol.session.TeamInfo import TeamInfo
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+
+
+class LearnSettings(SessionSettings):
+	'''
+	Settings for learn protocol.
+	'''
+
+	def __init__(self, participants:List[TeamInfo] , deadline:Deadline) :
+		'''
+		@param participants the list of {@link PartyWithProfile} in clockwise
+		        order. There must be at least 2 to run the SAOP
+		        protocol. But SAOP can be initialized with less, for
+		        use in TournamentSettings.
+		        <table>
+		        <caption>Required parameters</caption>
+		        <tr>
+		        <td>persistentstate</td>
+		        <td>a {@link FileLocation}, serialized as json eg
+		        <code>"b29f3bf5-1dc4-499e-a676-7b2dbb864a03"</code> ,
+		        where the party's persistant state is stored; this
+		        must match the persistantstate used in earlier calls
+		        for e.g. {@link SAOPSettings}.</td>
+		        </tr>
+		
+		        <tr>
+		        <td>negotiationdata</td>
+		        <td>a List of {@link FileLocation}s, serialized as
+		        json, matching the negotiationdata used in earlier
+		        calls for eg {@link SAOPSettings}.</td>
+		        </tr>
+		        </table>
+		@param deadline     the deadline of the negotiation.
+		'''
+		self._participants = participants
+		self._deadline = deadline
+		if participants == None:
+			raise ValueError("participants must not be null")
+		if deadline == None:
+			raise ValueError("deadline must not be null")
+
+		for pwithp in self.getAllParties():
+			params = pwithp.getParty().getParameters()
+			statestr = params.get("persistentstate")
+			if not isinstance(statestr , str):
+				raise ValueError(
+						"persistentstate parameter containing UUID string is required, but found "\
+								+ str(statestr))
+			# check that it contains a UUID. Don't store it, we don't need
+			# it.
+			UUID(statestr)
+
+			# and check the negotiationdata
+			negotiationsstr = params.get("negotiationdata")
+			if not isinstance(negotiationsstr, list):
+				raise ValueError(
+					"negotiationdata parameter containing a List is required")
+
+			for negostr in  negotiationsstr:
+				if not isinstance(negostr,str):
+					raise ValueError(
+						"The list in negotiationdata must contain UUID strings but found "\
+							+ str(negostr))
+				UUID(negostr)
+
+	def getMaxRunTime(self)->float:
+		return self._deadline.getDuration() / 1000.
+
+	def getProtocol(self, logger:Reporter )->SessionProtocol :
+		from geniusweb.protocol.session.learn.LearnState import LearnState
+		from geniusweb.protocol.session.learn.Learn import Learn
+		return Learn(LearnState([],[],None, self), logger)
+
+	def getTeams(self)->List[TeamInfo] :
+		return list(self._participants)
+
+	def getParticipants(self) -> List[TeamInfo] :
+		return list(self._participants)
+
+	def getDeadline(self)->Deadline :
+		'''
+		@return the deadline for this negotiation
+		'''
+		return self._deadline
+
+	def getAllParties(self)->List[PartyWithProfile] :
+		return  [particip.getParties()[0]
+			 for particip in self._participants ]
+
+	def getTeamSize(self)->int:
+		return 1
+
+	def __repr__(self):
+		return "LearnSettings" + str(self._participants) +\
+			"," + str(self._deadline) + "]"
+
+	def __hash__(self):
+		return hash((self._deadline, tuple(self._participants)))
+
+	
+	def __eq__(self, other) -> bool:
+		return isinstance(other, self.__class__) and \
+			self._participants == other._participants and \
+			self._deadline==other._deadline 
+
+	def With(self, team:TeamInfo)->SessionSettings :
+		if team.getSize() != 1:
+			raise ValueError(
+					"Team must be size 1 but found " + str(team))
+		newparts = list(self._participants)
+		newparts.append(team)
+		return LearnSettings(newparts, self._deadline)
Index: geniuswebcore/geniusweb/protocol/session/learn/LearnState.py
===================================================================
--- geniuswebcore/geniusweb/protocol/session/learn/LearnState.py	(revision 71)
+++ geniuswebcore/geniusweb/protocol/session/learn/LearnState.py	(revision 71)
@@ -0,0 +1,99 @@
+from typing import List, Dict, Set, Optional
+
+from geniusweb.actions.Action import Action
+from geniusweb.actions.LearningDone import LearningDone
+from geniusweb.actions.PartyId import PartyId
+from geniusweb.inform.Agreements import Agreements
+from geniusweb.progress.Progress import Progress
+from geniusweb.protocol.ProtocolException import ProtocolException
+from geniusweb.protocol.session.DefaultSessionState import DefaultSessionState
+from geniusweb.protocol.session.SessionResult import SessionResult
+from geniusweb.protocol.session.learn.LearnSettings import LearnSettings
+from geniusweb.references.PartyWithProfile import PartyWithProfile
+
+class LearnState (DefaultSessionState["LearnState", "LearnSettings"]):
+	'''
+	Stores the state for Learn protocol. Immutable.
+	'''
+
+	def __init__(self, actions:List[Action] , conns:List[PartyId] ,
+			progress:Optional[Progress] , settings:LearnSettings ,
+			partyprofiles:Dict[PartyId, PartyWithProfile]={}, 
+			e:Optional[ProtocolException]=None ):
+		'''
+		@param actions       the actions done by the parties
+		@param conns         the existing party connections. we assume ownership
+		            of this so it should not be modified although
+		            connections may of course break. If null, default
+		            empty list is used. Can be less than 2 parties in
+		            the first phases of the setup.
+		@param progress      the {@link Progress} line. can be null
+		@param settings      the {@link LearnSettings}
+		@param partyprofiles map with the {@link PartyWithProfile} for connected
+		            parties. 
+		@param e    the {@link ProtocolException}, or null if none
+		            occurred.
+		'''
+		
+		super().__init__(actions, conns, progress, settings, partyprofiles, e)
+
+# 	def LearnState(LearnSettings settings) {
+# 		'''
+# 		Creates the initial state from the given settings and progress=null
+# 		
+# 		@param settings the {@link LearnSettings}
+# 		'''
+# 		this(Collections.emptyList(), Collections.emptyList(), null, settings,
+# 				null, null);
+# 
+# 	}
+
+	def With(self, actions:List[Action] , conns:List[PartyId],
+			progr:Optional[Progress] ,  settings:LearnSettings, 
+			partyprofiles:Dict[PartyId, PartyWithProfile] , 
+			e:Optional[ProtocolException] ) -> "LearnState" :
+		return LearnState(actions, conns, progr, settings, partyprofiles,
+				e)
+
+	def getAgreements(self)->Agreements :
+		return Agreements()
+
+	def getResult(self)->SessionResult :
+		return SessionResult(self.getPartyProfiles(), self.getAgreements(),
+				{}, None)
+
+	def isFinal(self, currentTimeMs:int) ->bool:
+		done = self._getDoneLearning()
+		# if done is empty we're probably still starting up.
+		return super().isFinal(currentTimeMs)\
+				or (len(done)!=0 and set(self.getConnections()).issubset(done))
+
+	def WithException(self, e:ProtocolException) ->"LearnState" :
+		'''
+		@param e the error that occured
+		@return a new state with the error set.
+		'''
+		return LearnState(self.getActions(), self.getConnections(), self.getProgress(),
+				self.getSettings(), self.getPartyProfiles(), e)
+
+	def checkAction(self, actor:PartyId , action:Action ) ->Optional[str]:
+		'''
+		@param actor  the known real actor that did this action
+		@param action an {@link Action}
+		@return null if action seems ok, or message explaining why not.
+		'''
+		msg = super().checkAction(actor, action)
+		if msg != None:
+			return msg
+		if actor in self._getDoneLearning():
+			return "actor already was done learning"
+		if not isinstance(action, LearningDone):
+			return "Action " + str(action) + " is not allowed"
+		return None
+
+	def _getDoneLearning(self)->Set[PartyId] :
+		'''
+		@return set of parties that reported they are done with learning.
+		'''
+		return set([party.getActor() for party in self.getActions()  ])
+	
Index: geniuswebcore/geniusweb/protocol/session/saop/SAOPState.py
===================================================================
--- geniuswebcore/geniusweb/protocol/session/saop/SAOPState.py	(revision 70)
+++ geniuswebcore/geniusweb/protocol/session/saop/SAOPState.py	(revision 71)
@@ -18,6 +18,5 @@
 from geniusweb.references.PartyWithProfile import PartyWithProfile
 
-#CHECK WHY DO THE TYPE VARIABLES HAVE TO BE SWAPPED HERE?
-class SAOPState (DefaultSessionState[ "SAOPSettings","SAOPState"]): 
+class SAOPState (DefaultSessionState[ "SAOPState","SAOPSettings"]):  
 	'''
 	Immutable.
