diff --git a/etc/message.xml b/etc/message.xml
index 1e6e30ec6c77bb0bcfc2cf87b583627a358cca91..18f61198ce6fbcb6cd3a1d0d9d11077704378a1d 100644
--- a/etc/message.xml
+++ b/etc/message.xml
@@ -362,7 +362,7 @@
 					<boolean>false</boolean>
 				</map>
 
-				<key>avatarpickrequest</key>
+				<key>avatarpicksrequest</key>
 				<map>
 					<key>service_name</key>
 					<string>avatar-pick</string>
diff --git a/indra/lib/python/indra/base/lluuid.py b/indra/lib/python/indra/base/lluuid.py
index eb0c557b0456e83f3cf4d044a38a097373e570d3..609c8cc2616c314bb14e39bd51442fd8805dc214 100644
--- a/indra/lib/python/indra/base/lluuid.py
+++ b/indra/lib/python/indra/base/lluuid.py
@@ -212,6 +212,10 @@ def xor(self, rhs):
                      _int2binstr(v3,4) + \
                      _int2binstr(v4,4) 
 
+
+# module-level null constant
+NULL = UUID()
+
 def printTranslatedMemory(four_hex_uints):
     """
     We expect to get the string as four hex units. eg:
diff --git a/indra/lib/python/indra/ipc/mysql_pool.py b/indra/lib/python/indra/ipc/mysql_pool.py
index 4a265a1f2ec94edf491b8ec27a2770c1d264c4eb..6288a4c73227d90f53791a69c590b625a5d4e908 100644
--- a/indra/lib/python/indra/ipc/mysql_pool.py
+++ b/indra/lib/python/indra/ipc/mysql_pool.py
@@ -97,7 +97,7 @@ def put(self, conn):
         except (AttributeError, DeadProcess), e:
             conn = self.create()
         # TODO figure out if we're still connected to the database
-        if conn:
+        if conn is not None:
             Pool.put(self, conn)
         else:
             self.current_size -= 1
diff --git a/indra/lib/python/indra/util/named_query.py b/indra/lib/python/indra/util/named_query.py
index 019eb6306b8c108487a8101d944ac3336f311bf1..c3bdd046fd8c98bd7782b28089a53883aec01d78 100644
--- a/indra/lib/python/indra/util/named_query.py
+++ b/indra/lib/python/indra/util/named_query.py
@@ -28,20 +28,33 @@
 $/LicenseInfo$
 """
 
+import errno
 import MySQLdb
+import MySQLdb.cursors
 import os
 import os.path
+import re
 import time
 
+#import sys # *TODO: remove. only used in testing.
+#import pprint # *TODO: remove. only used in testing.
+
+try:
+    set = set
+except NameError:
+    from sets import Set as set
+
 from indra.base import llsd
 from indra.base import config
-from indra.ipc import russ
 
 _g_named_manager = None
 
-# this function is entirely intended for testing purposes,
-# because it's tricky to control the config from inside a test
 def _init_g_named_manager(sql_dir = None):
+    """Initializes a global NamedManager object to point at a
+    specified named queries hierarchy.
+
+    This function is intended entirely for testing purposes,
+    because it's tricky to control the config from inside a test."""
     if sql_dir is None:
         sql_dir = config.get('named-query-base-dir')
     global _g_named_manager
@@ -49,14 +62,14 @@ def _init_g_named_manager(sql_dir = None):
         os.path.abspath(os.path.realpath(sql_dir)))
 
 def get(name):
-    "@brief get the named query object to be used to perform queries"
+    "Get the named query object to be used to perform queries"
     if _g_named_manager is None:
         _init_g_named_manager()
     return _g_named_manager.get(name)
 
-def sql(name, params):
+def sql(connection, name, params):
     # use module-global NamedQuery object to perform default substitution
-    return get(name).sql(params)
+    return get(name).sql(connection, params)
 
 def run(connection, name, params, expect_rows = None):
     """\
@@ -72,66 +85,243 @@ def run(connection, name, params, expect_rows = None):
     return get(name).run(connection, params, expect_rows)
 
 class ExpectationFailed(Exception):
+    """ Exception that is raised when an expectation for an sql query
+    is not met."""
     def __init__(self, message):
+        Exception.__init__(self, message)
         self.message = message
 
 class NamedQuery(object):
     def __init__(self, name, filename):
-        self._stat_interval = 5000  # 5 seconds
+        """ Construct a NamedQuery object.  The name argument is an
+        arbitrary name as a handle for the query, and the filename is
+        a path to a file containing an llsd named query document."""
+        self._stat_interval_seconds = 5  # 5 seconds
         self._name = name
         self._location = filename
+        self._alternative = dict()
+        self._last_mod_time = 0
+        self._last_check_time = 0
+        self.deleted = False
         self.load_contents()
 
     def name(self):
+        """ The name of the query. """
         return self._name
 
     def get_modtime(self):
-        return os.path.getmtime(self._location)
+        """ Returns the mtime (last modified time) of the named query
+        file, if such exists."""
+        if self._location:
+            return os.path.getmtime(self._location)
+        return 0
 
     def load_contents(self):
-        self._contents = llsd.parse(open(self._location).read())
+        """ Loads and parses the named query file into self.  Does
+        nothing if self.location is nonexistant."""
+        if self._location:
+            self._reference_contents(llsd.parse(open(self._location).read()))
+            # Check for alternative implementations
+            try:
+                for name, alt in self._contents['alternative'].items():
+                    nq = NamedQuery(name, None)
+                    nq._reference_contents(alt)
+                    self._alternative[name] = nq
+            except KeyError, e:
+                pass
+            self._last_mod_time = self.get_modtime()
+            self._last_check_time = time.time()
+
+    def _reference_contents(self, contents):
+        "Helper method which builds internal structure from parsed contents"
+        self._contents = contents
         self._ttl = int(self._contents.get('ttl', 0))
         self._return_as_map = bool(self._contents.get('return_as_map', False))
         self._legacy_dbname = self._contents.get('legacy_dbname', None)
-        self._legacy_query = self._contents.get('legacy_query', None)
-        self._options = self._contents.get('options', {})
-        self._base_query = self._contents['base_query']
 
-        self._last_mod_time = self.get_modtime()
-        self._last_check_time = time.time()
+        # reset these before doing the sql conversion because we will
+        # read them there. reset these while loading so we pick up
+        # changes.
+        self._around = set()
+        self._append = set()
+        self._integer = set()
+        self._options = self._contents.get('dynamic_where', {})
+        for key in self._options:
+            if isinstance(self._options[key], basestring):
+                self._options[key] = self._convert_sql(self._options[key])
+            elif isinstance(self._options[key], list):
+                lines = []
+                for line in self._options[key]:
+                    lines.append(self._convert_sql(line))
+                self._options[key] = lines
+            else:
+                moreopt = {}
+                for kk in self._options[key]:
+                    moreopt[kk] = self._convert_sql(self._options[key][kk]) 
+                self._options[key] = moreopt
+        self._base_query = self._convert_sql(self._contents['base_query'])
+        self._query_suffix = self._convert_sql(
+            self._contents.get('query_suffix', ''))
+
+    def _convert_sql(self, sql):
+        """convert the parsed sql into a useful internal structure.
+
+        This function has to turn the named query format into a pyformat
+        style. It also has to look for %:name% and :name% and
+        ready them for use in LIKE statements"""
+        if sql:
+            #print >>sys.stderr, "sql:",sql
+            expr = re.compile("(%?):([a-zA-Z][a-zA-Z0-9_-]*)%")
+            sql = expr.sub(self._prepare_like, sql)
+            expr = re.compile("#:([a-zA-Z][a-zA-Z0-9_-]*)")
+            sql = expr.sub(self._prepare_integer, sql)
+            expr = re.compile(":([a-zA-Z][a-zA-Z0-9_-]*)")
+            sql = expr.sub("%(\\1)s", sql)
+        return sql
+
+    def _prepare_like(self, match):
+        """This function changes LIKE statement replace behavior
+
+        It works by turning %:name% to %(_name_around)s and :name% to
+        %(_name_append)s. Since a leading '_' is not a valid keyname
+        input (enforced via unit tests), it will never clash with
+        existing keys. Then, when building the statement, the query
+        runner will generate corrected strings."""
+        if match.group(1) == '%':
+            # there is a leading % so this is treated as prefix/suffix
+            self._around.add(match.group(2))
+            return "%(" + self._build_around_key(match.group(2)) + ")s"
+        else:
+            # there is no leading %, so this is suffix only
+            self._append.add(match.group(2))
+            return "%(" + self._build_append_key(match.group(2)) + ")s"
+
+    def _build_around_key(self, key):
+        return "_" + key + "_around"
+
+    def _build_append_key(self, key):
+        return "_" + key + "_append"
+
+    def _prepare_integer(self, match):
+        """This function adjusts the sql for #:name replacements
+
+        It works by turning #:name to %(_name_as_integer)s. Since a
+        leading '_' is not a valid keyname input (enforced via unit
+        tests), it will never clash with existing keys. Then, when
+        building the statement, the query runner will generate
+        corrected strings."""
+        self._integer.add(match.group(1))
+        return "%(" + self._build_integer_key(match.group(1)) + ")s"
+
+    def _build_integer_key(self, key):
+        return "_" + key + "_as_integer"
+
+    def _strip_wildcards_to_list(self, value):
+        """Take string, and strip out the LIKE special characters.
+
+        Technically, this is database dependant, but postgresql and
+        mysql use the same wildcards, and I am not aware of a general
+        way to handle this. I think you need a sql statement of the
+        form:
+
+        LIKE_STRING( [ANY,ONE,str]... )
+
+        which would treat ANY as their any string, and ONE as their
+        single glyph, and str as something that needs database
+        specific encoding to not allow any % or _ to affect the query.
+
+        As it stands, I believe it's impossible to write a named query
+        style interface which uses like to search the entire space of
+        text available. Imagine the query:
+
+        % of brain used by average linden
+
+        In order to search for %, it must be escaped, so once you have
+        escaped the string to not do wildcard searches, and be escaped
+        for the database, and then prepended the wildcard you come
+        back with one of:
+
+        1) %\% of brain used by average linden
+        2) %%% of brain used by average linden
+
+        Then, when passed to the database to be escaped to be database
+        safe, you get back:
+        
+        1) %\\% of brain used by average linden
+        : which means search for any character sequence, followed by a
+          backslash, followed by any sequence, followed by ' of
+          brain...'
+        2) %%% of brain used by average linden
+        : which (I believe) means search for a % followed by any
+          character sequence followed by 'of brain...'
+
+        Neither of which is what we want!
+
+        So, we need a vendor (or extention) for LIKE_STRING. Anyone
+        want to write it?"""
+        utf8_value = unicode(value, "utf-8")
+        esc_list = []
+        remove_chars = set(u"%_")
+        for glyph in utf8_value:
+            if glyph in remove_chars:
+                continue
+            esc_list.append(glyph.encode("utf-8"))
+        return esc_list
+
+    def delete(self):
+        """ Makes this query unusable by deleting all the members and
+        setting the deleted member.  This is desired when the on-disk
+        query has been deleted but the in-memory copy remains."""
+        # blow away all members except _name, _location, and deleted
+        name, location = self._name, self._location
+        for key in self.__dict__.keys():
+            del self.__dict__[key]
+        self.deleted = True
+        self._name, self._location = name, location
 
     def ttl(self):
+        """ Estimated time to live of this query. Used for web
+        services to set the Expires header."""
         return self._ttl
 
     def legacy_dbname(self):
         return self._legacy_dbname
 
-    def legacy_query(self):
-        return self._legacy_query
-
     def return_as_map(self):
+        """ Returns true if this query is configured to return its
+        results as a single map (as opposed to a list of maps, the
+        normal behavior)."""
+        
         return self._return_as_map
 
-    def run(self, connection, params, expect_rows = None, use_dictcursor = True):
-        """\
-@brief given a connection, run a named query with the params
+    def for_schema(self, db_name):
+        "Look trough the alternates and return the correct query"
+        try:
+            return self._alternative[db_name]
+        except KeyError, e:
+            pass
+        return self
 
-Note that this function will fetch ALL rows. We do this because it
-opens and closes the cursor to generate the values, and this isn't a generator so the
-cursor has no life beyond the method call.
-@param cursor The connection to use (this generates its own cursor for the query)
-@param name The name of the query to run
-@param params The parameters passed into the query
-@param expect_rows The number of rows expected. Set to 1 if return_as_map is true.  Raises ExpectationFailed if the number of returned rows doesn't exactly match.  Kind of a hack.
-@param use_dictcursor Set to false to use a normal cursor and manually convert the rows to dicts.
-@return Returns the result set as a list of dicts, or, if the named query has return_as_map set to true, returns a single dict.
+    def run(self, connection, params, expect_rows = None, use_dictcursor = True):
+        """given a connection, run a named query with the params
+
+        Note that this function will fetch ALL rows. We do this because it
+        opens and closes the cursor to generate the values, and this 
+        isn't a generator so the cursor has no life beyond the method call.
+
+        @param cursor The connection to use (this generates its own cursor for the query)
+        @param name The name of the query to run
+        @param params The parameters passed into the query
+        @param expect_rows The number of rows expected. Set to 1 if return_as_map is true.  Raises ExpectationFailed if the number of returned rows doesn't exactly match.  Kind of a hack.
+        @param use_dictcursor Set to false to use a normal cursor and manually convert the rows to dicts.
+        @return Returns the result set as a list of dicts, or, if the named query has return_as_map set to true, returns a single dict.
         """
         if use_dictcursor:
             cursor = connection.cursor(MySQLdb.cursors.DictCursor)
         else:
             cursor = connection.cursor()
         
-        statement = self.sql(params)
+        statement = self.sql(connection, params)
         #print "SQL:", statement
         rows = cursor.execute(statement)
         
@@ -169,47 +359,152 @@ def run(self, connection, params, expect_rows = None, use_dictcursor = True):
             return result_set[0]
         return result_set
 
-    def sql(self, params):
+    def sql(self, connection, params):
+        """ Generates an SQL statement from the named query document
+        and a dictionary of parameters.
+
+        """
         self.refresh()
 
         # build the query from the options available and the params
         base_query = []
         base_query.append(self._base_query)
+        #print >>sys.stderr, "base_query:",base_query
         for opt, extra_where in self._options.items():
-            if opt in params and (params[opt] == 0 or params[opt]):
-                if type(extra_where) in (dict, list, tuple):
+            if type(extra_where) in (dict, list, tuple):
+                if opt in params:
                     base_query.append(extra_where[params[opt]])
-                else:
+            else:
+                if opt in params and params[opt]:
                     base_query.append(extra_where)
-
+        if self._query_suffix:
+            base_query.append(self._query_suffix)
+        #print >>sys.stderr, "base_query:",base_query
         full_query = '\n'.join(base_query)
-        
-        # do substitution
-        sql = russ.format(full_query, params)
+
+        # Go through the query and rewrite all of the ones with the
+        # @:name syntax.
+        rewrite = _RewriteQueryForArray(params)
+        expr = re.compile("@%\(([a-zA-Z][a-zA-Z0-9_-]*)\)s")
+        full_query = expr.sub(rewrite.operate, full_query)
+        params.update(rewrite.new_params)
+
+        # build out the params for like. We only have to do this
+        # parameters which were detected to have ued the where syntax
+        # during load.
+        #
+        # * treat the incoming string as utf-8
+        # * strip wildcards
+        # * append or prepend % as appropriate
+        new_params = {}
+        for key in params:
+            if key in self._around:
+                new_value = ['%']
+                new_value.extend(self._strip_wildcards_to_list(params[key]))
+                new_value.append('%')
+                new_params[self._build_around_key(key)] = ''.join(new_value)
+            if key in self._append:
+                new_value = self._strip_wildcards_to_list(params[key])
+                new_value.append('%')
+                new_params[self._build_append_key(key)] = ''.join(new_value)
+            if key in self._integer:
+                new_params[self._build_integer_key(key)] = int(params[key])
+        params.update(new_params)
+
+        # do substitution using the mysql (non-standard) 'literal'
+        # function to do the escaping.
+        sql = full_query % connection.literal(params)
         return sql
 
     def refresh(self):
-        # only stat the file every so often
+        """ Refresh self from the file on the filesystem.
+
+        This is optimized to be callable as frequently as you wish,
+        without adding too much load.  It does so by only stat-ing the
+        file every N seconds, where N defaults to 5 and is
+        configurable through the member _stat_interval_seconds.  If the stat
+        reveals that the file has changed, refresh will re-parse the
+        contents of the file and use them to update the named query
+        instance.  If the stat reveals that the file has been deleted,
+        refresh will call self.delete to make the in-memory
+        representation unusable."""
         now = time.time()
-        if(now - self._last_check_time > self._stat_interval):
+        if(now - self._last_check_time > self._stat_interval_seconds):
             self._last_check_time = now
-            modtime = self.get_modtime()
-            if(modtime > self._last_mod_time):
-                self.load_contents()
+            try:
+                modtime = self.get_modtime()
+                if(modtime > self._last_mod_time):
+                    self.load_contents()
+            except OSError, e:
+                if e.errno == errno.ENOENT: # file not found
+                    self.delete() # clean up self
+                raise  # pass the exception along to the caller so they know that this query disappeared
 
 class NamedQueryManager(object):
+    """ Manages the lifespan of NamedQuery objects, drawing from a
+    directory hierarchy of named query documents.
+
+    In practice this amounts to a memory cache of NamedQuery objects."""
+    
     def __init__(self, named_queries_dir):
+        """ Initializes a manager to look for named queries in a
+        directory."""
         self._dir = os.path.abspath(os.path.realpath(named_queries_dir))
         self._cached_queries = {}
 
-    def sql(self, name, params):
+    def sql(self, connection, name, params):
         nq = self.get(name)
-        return nq.sql(params)
+        return nq.sql(connection, params)
         
     def get(self, name):
-        # new up/refresh a NamedQuery based on the name
+        """ Returns a NamedQuery instance based on the name, either
+        from memory cache, or by parsing from disk.
+
+        The name is simply a relative path to the directory associated
+        with the manager object.  Before returning the instance, the
+        NamedQuery object is cached in memory, so that subsequent
+        accesses don't have to read from disk or do any parsing.  This
+        means that NamedQuery objects returned by this method are
+        shared across all users of the manager object.
+        NamedQuery.refresh is used to bring the NamedQuery objects in
+        sync with the actual files on disk."""
         nq = self._cached_queries.get(name)
         if nq is None:
             nq = NamedQuery(name, os.path.join(self._dir, name))
             self._cached_queries[name] = nq
+        else:
+            try:
+                nq.refresh()
+            except OSError, e:
+                if e.errno == errno.ENOENT: # file not found
+                    del self._cached_queries[name]
+                raise # pass exception along to caller so they know that the query disappeared
+
         return nq
+
+class _RewriteQueryForArray(object):
+    "Helper class for rewriting queries with the @:name syntax"
+    def __init__(self, params):
+        self.params = params
+        self.new_params = dict()
+
+    def operate(self, match):
+        "Given a match, return the string that should be in use"
+        key = match.group(1)
+        value = self.params[key]
+        if type(value) in (list,tuple):
+            rv = []
+            for idx in range(len(value)):
+                new_key = "_" + key + "_" + str(idx)
+                self.new_params[new_key] = value[idx]
+                rv.append("%(" + new_key + ")s")
+            return ','.join(rv)
+        else:
+            # not something that can be expanded, so just drop the
+            # leading @ in the front of the match. This will mean that
+            # the single value we have, be it a string, int, whatever
+            # (other than dict) will correctly show up, eg:
+            #
+            # where foo in (@:foobar) -- foobar is a string, so we get
+            # where foo in (:foobar)
+            return match.group(0)[1:]
diff --git a/indra/llcommon/llsd.cpp b/indra/llcommon/llsd.cpp
index 829ea25e38fa9c14a1b19da963cbb8f4e53d290e..ecca4c9b71c6bdab37a0857c6f26a5fcb94ec2a9 100644
--- a/indra/llcommon/llsd.cpp
+++ b/indra/llcommon/llsd.cpp
@@ -35,6 +35,7 @@
 #include "llerror.h"
 #include "../llmath/llmath.h"
 #include "llformat.h"
+#include "llsdserialize.h"
 
 #ifndef LL_RELEASE_FOR_DOWNLOAD
 #define NAME_UNNAMED_NAMESPACE
@@ -765,6 +766,44 @@ const LLSD& LLSD::operator[](Integer i) const
 U32 LLSD::allocationCount()				{ return Impl::sAllocationCount; }
 U32 LLSD::outstandingCount()			{ return Impl::sOutstandingCount; }
 
+static const char *llsd_dump(const LLSD &llsd, bool useXMLFormat)
+{
+	// sStorage is used to hold the string representation of the llsd last
+	// passed into this function.  If this function is never called (the
+	// normal case when not debugging), nothing is allocated.  Otherwise
+	// sStorage will point to the result of the last call.  This will actually
+	// be one leak, but since this is used only when running under the
+	// debugger, it should not be an issue.
+	static char *sStorage = NULL;
+	delete[] sStorage;
+	std::string out_string;
+	{
+		std::ostringstream out;
+		if (useXMLFormat)
+			out << LLSDXMLStreamer(llsd);
+		else
+			out << LLSDNotationStreamer(llsd);
+		out_string = out.str();
+	}
+	int len = out_string.length();
+	sStorage = new char[len + 1];
+	memcpy(sStorage, out_string.c_str(), len);
+	sStorage[len] = '\0';
+	return sStorage;
+}
+
+/// Returns XML version of llsd -- only to be called from debugger
+const char *LLSD::dumpXML(const LLSD &llsd)
+{
+	return llsd_dump(llsd, true);
+}
+
+/// Returns Notation version of llsd -- only to be called from debugger
+const char *LLSD::dump(const LLSD &llsd)
+{
+	return llsd_dump(llsd, false);
+}
+
 LLSD::map_iterator			LLSD::beginMap()		{ return makeMap(impl).beginMap(); }
 LLSD::map_iterator			LLSD::endMap()			{ return makeMap(impl).endMap(); }
 LLSD::map_const_iterator	LLSD::beginMap() const	{ return safe(impl).beginMap(); }
diff --git a/indra/llcommon/llsd.h b/indra/llcommon/llsd.h
index 65ba7ddc4f76f3d00a51c3b5cc45ae53e00fc2f4..1ba57b1e958dba3901da5b12267b7fa5db4bdfb6 100644
--- a/indra/llcommon/llsd.h
+++ b/indra/llcommon/llsd.h
@@ -331,6 +331,16 @@ class LLSD
 		static U32 allocationCount();	///< how many Impls have been made
 		static U32 outstandingCount();	///< how many Impls are still alive
 	//@}
+
+private:
+	/** @name Debugging Interface */
+	//@{
+		/// Returns XML version of llsd -- only to be called from debugger
+		static const char *dumpXML(const LLSD &llsd);
+
+		/// Returns Notation version of llsd -- only to be called from debugger
+		static const char *dump(const LLSD &llsd);
+	//@}
 };
 
 struct llsd_select_bool : public std::unary_function<LLSD, LLSD::Boolean>
diff --git a/indra/llmessage/llservicebuilder.cpp b/indra/llmessage/llservicebuilder.cpp
index 4fb3530c15ab5931439acf562a494f2559a52fa9..22e5c4af43d3aded1a2167a2ddd32403b51321a2 100644
--- a/indra/llmessage/llservicebuilder.cpp
+++ b/indra/llmessage/llservicebuilder.cpp
@@ -116,6 +116,104 @@ std::string LLServiceBuilder::buildServiceURI(
 	// Find the Service Name
 	if(!service_url.empty() && option_map.isMap())
 	{
+		// throw in a ridiculously large limiter to make sure we don't
+		// loop forever with bad input.
+		int iterations = 100;
+		bool keep_looping = true;
+		while(keep_looping)
+		{
+			if(0 == --iterations)
+			{
+				keep_looping = false;
+			}
+
+			int depth = 0;
+			int deepest = 0;
+			bool find_match = false;
+			std::string::iterator iter(service_url.begin());
+			std::string::iterator end(service_url.end());
+			std::string::iterator deepest_node(service_url.end());
+			std::string::iterator deepest_node_end(service_url.end());
+			for(; iter != end; ++iter)
+			{
+				switch(*iter)
+				{
+				case '{':
+					++depth;
+					if(depth > deepest)
+					{
+						deepest = depth;
+						deepest_node = iter;
+						find_match = true;
+					}
+					break;
+				case '}':
+					--depth;
+					if(find_match)
+					{
+						deepest_node_end = iter;
+						find_match = false;
+					}
+					break;
+				default:
+					break;
+				}
+			}
+			if((deepest_node == end) || (deepest_node_end == end))
+			{
+				break;
+			}
+
+			// *NOTE: since the c++ implementation only understands
+			// params and straight string substitution, so it's a
+			// known distance of 2 to skip the directive.
+			std::string key(deepest_node + 2, deepest_node_end);
+			LLSD value = option_map[key];
+			switch(*(deepest_node + 1))
+			{
+			case '$':
+				if(value.isDefined())
+				{
+					service_url.replace(
+						deepest_node,
+						deepest_node_end + 1,
+						value.asString());
+				}
+				else
+				{
+					llinfos << "Unknown key: " << key << llendl;
+					keep_looping = false;
+				}
+				break;
+			case '%':
+				{
+					std::string query_str = LLURI::mapToQueryString(value);
+					service_url.replace(
+						deepest_node,
+						deepest_node_end + 1,
+						query_str);
+				}
+				break;
+			default:
+				llinfos << "Unknown directive: " << *(deepest_node + 1)
+					<< llendl;
+				keep_looping = false;
+				break;
+			}
+		}
+	}
+	if (service_url.find('{') != std::string::npos)
+	{
+		llwarns << "Constructed a likely bogus service URL: " << service_url
+			<< llendl;
+	}
+	return service_url;
+}
+
+
+
+// Old, not as good implementation. Phoenix 2007-10-15
+#if 0
 		// Do brace replacements - NOT CURRENTLY RECURSIVE
 		for(LLSD::map_const_iterator option_itr = option_map.beginMap();
 			option_itr != option_map.endMap();
@@ -157,3 +255,4 @@ std::string LLServiceBuilder::buildServiceURI(
 
 	return service_url;
 }
+#endif
diff --git a/indra/llmessage/llurlrequest.cpp b/indra/llmessage/llurlrequest.cpp
index 857f8048595489b0254278a6fa1f526b0f222e41..6a09a8bbecc36059b4c529638c3ef61d0805fc54 100644
--- a/indra/llmessage/llurlrequest.cpp
+++ b/indra/llmessage/llurlrequest.cpp
@@ -480,6 +480,7 @@ bool LLURLRequest::configure()
 				mDetail->mHeaders);
 		}
 		curl_easy_setopt(mDetail->mCurl, CURLOPT_URL, mDetail->mURL);
+		lldebugs << "URL: " << mDetail->mURL << llendl;
 		curl_multi_add_handle(mDetail->mCurlMulti, mDetail->mCurl);
 		mDetail->mNeedToRemoveEasyHandle = true;
 	}
diff --git a/indra/llmessage/llurlrequest.h b/indra/llmessage/llurlrequest.h
index f335316ab6472cb014560551e2bfda2f660af318..7cb6b6a6184d657bc8343d9831b3e28fe18afa46 100644
--- a/indra/llmessage/llurlrequest.h
+++ b/indra/llmessage/llurlrequest.h
@@ -42,6 +42,7 @@
 #include <string>
 #include "lliopipe.h"
 #include "llchainio.h"
+#include "llerror.h"
 
 class LLURLRequestDetail;
 
@@ -62,6 +63,7 @@ class LLURLRequestComplete;
  */
 class LLURLRequest : public LLIOPipe
 {
+	LOG_CLASS(LLURLRequest);
 public:
 	/** 
 	 * @brief This enumeration is for specifying the type of request.
diff --git a/indra/llmessage/message.cpp b/indra/llmessage/message.cpp
index d446730c335f5707267c40774575f4f270c55aff..a1b63ead75bf90e7a7dbf4a9fd1eba4ed0172e6b 100644
--- a/indra/llmessage/message.cpp
+++ b/indra/llmessage/message.cpp
@@ -1100,6 +1100,17 @@ void LLMessageSystem::forwardReliable(const U32 circuit_code)
 	sendReliable(findHost(circuit_code));
 }
 
+S32 LLMessageSystem::forwardReliable(	const LLHost &host, 
+										S32 retries, 
+										BOOL ping_based_timeout,
+										F32 timeout, 
+										void (*callback)(void **,S32), 
+										void ** callback_data)
+{
+	copyMessageRtoS();
+	return sendReliable(host, retries, ping_based_timeout, timeout, callback, callback_data);
+}
+
 S32 LLMessageSystem::flushSemiReliable(const LLHost &host, void (*callback)(void **,S32), void ** callback_data)
 {
 	F32 timeout; 
diff --git a/indra/llmessage/message.h b/indra/llmessage/message.h
index 3381ece222609bb7c96d9a2b9b0625d7588f4d1c..4debcddf990e5176d46559578e7e6e64079ff7c6 100644
--- a/indra/llmessage/message.h
+++ b/indra/llmessage/message.h
@@ -464,6 +464,13 @@ class LLMessageSystem
 	void    forwardMessage(const LLHost &host);
 	void    forwardReliable(const LLHost &host);
 	void    forwardReliable(const U32 circuit_code);
+	S32 forwardReliable(
+		const LLHost &host, 
+		S32 retries, 
+		BOOL ping_based_timeout,
+		F32 timeout, 
+		void (*callback)(void **,S32), 
+		void ** callback_data);
 
 	LLHTTPClient::ResponderPtr createResponder(const std::string& name);
 	S32		sendMessage(const LLHost &host);
diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp
index a6a9447e39527dcef3f1082935ef3769ef76a590..c319ef97af9e8dbba0fd4af11ed81684140561a4 100644
--- a/indra/newview/llstartup.cpp
+++ b/indra/newview/llstartup.cpp
@@ -2117,7 +2117,6 @@ BOOL idle_startup()
 		msg->setHandlerFuncFast(_PREHASH_PreloadSound,				process_preload_sound);
 		msg->setHandlerFuncFast(_PREHASH_AttachedSound,				process_attached_sound);
 		msg->setHandlerFuncFast(_PREHASH_AttachedSoundGainChange,	process_attached_sound_gain_change);
-		//msg->setHandlerFuncFast(_PREHASH_AttachedSoundCutoffRadius,	process_attached_sound_cutoff_radius);
 
 		llinfos << "Initialization complete" << llendl;
 
@@ -2771,7 +2770,6 @@ void register_viewer_callbacks(LLMessageSystem* msg)
 	msg->setHandlerFuncFast(_PREHASH_MeanCollisionAlert,             process_mean_collision_alert_message,  NULL);
 	msg->setHandlerFunc("ViewerFrozenMessage",             process_frozen_message);
 
-	//msg->setHandlerFuncFast(_PREHASH_RequestAvatarInfo,		process_avatar_info_request);
 	msg->setHandlerFuncFast(_PREHASH_NameValuePair,			process_name_value);
 	msg->setHandlerFuncFast(_PREHASH_RemoveNameValuePair,	process_remove_name_value);
 	msg->setHandlerFuncFast(_PREHASH_AvatarAnimation,		process_avatar_animation);
diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp
index 2011275bdb2e1365d032e1a8bcde9332da13f5cc..7f349fe65b5894047787ec2187aee4aff61ce341 100644
--- a/indra/newview/llviewermessage.cpp
+++ b/indra/newview/llviewermessage.cpp
@@ -3398,29 +3398,6 @@ void process_attached_sound_gain_change(LLMessageSystem *mesgsys, void **user_da
 	objectp->adjustAudioGain(gain);
 }
 
-/* Unused July 2006
-void process_attached_sound_cutoff_radius(LLMessageSystem *mesgsys, void **user_data)
-{
-	F32 radius = 0;
-	LLUUID object_guid;
-	LLViewerObject *objectp = NULL;
-
-	mesgsys->getUUIDFast(_PREHASH_DataBlock, _PREHASH_ObjectID, object_guid);
-
-	if (!((objectp = gObjectList.findObject(object_guid))))
-	{
-		// we don't know about this object, just bail
-		return;
-	}
-
-	mesgsys->getF32Fast(_PREHASH_DataBlock, _PREHASH_Radius, radius);
-
-	if (gAudiop)
-	{
-//		gAudiop->attachToObject(sound_guid, object_guid, gain, priority, flags);
-	}
-}
-*/
 
 void process_health_message(LLMessageSystem *mesgsys, void **user_data)
 {
@@ -3566,19 +3543,6 @@ void process_sim_stats(LLMessageSystem *msg, void **user_data)
 }
 
 
-// This info is requested by the simulator when the agent first logs in
-// or when it moves into a simulator in which it did not already have
-// a child agent.
-/*
-void process_avatar_info_request(LLMessageSystem *mesgsys, void **user_data)
-{
-	llinfos << "process_avatar_info_request()" << llendl;
-
-	// Send the avatar appearance (parameters and texture entry UUIDs)
-	gAgent.sendAgentSetAppearance();
-	send_agent_update(TRUE, TRUE);
-}*/
-
 
 void process_avatar_animation(LLMessageSystem *mesgsys, void **user_data)
 {
diff --git a/indra/newview/llviewermessage.h b/indra/newview/llviewermessage.h
index 2c5005c167cf16ad0b657fb603349b3c19b8df3c..f7739f0871984ae029552282ad933ba6a162554e 100644
--- a/indra/newview/llviewermessage.h
+++ b/indra/newview/llviewermessage.h
@@ -87,12 +87,10 @@ void process_sound_trigger(LLMessageSystem *mesgsys, void **user_data);
 void process_preload_sound(	LLMessageSystem *mesgsys, void **user_data);
 void process_attached_sound(	LLMessageSystem *mesgsys, void **user_data);
 void process_attached_sound_gain_change(	LLMessageSystem *mesgsys, void **user_data);
-//void process_attached_sound_cutoff_radius(	LLMessageSystem *mesgsys, void **user_data);
 void process_energy_statistics(LLMessageSystem *mesgsys, void **user_data);
 void process_health_message(LLMessageSystem *mesgsys, void **user_data);
 void process_sim_stats(LLMessageSystem *mesgsys, void **user_data);
 void process_shooter_agent_hit(LLMessageSystem* msg, void** user_data);
-void process_avatar_info_request(LLMessageSystem *mesgsys, void **user_data);
 void process_avatar_animation(LLMessageSystem *mesgsys, void **user_data);
 void process_avatar_appearance(LLMessageSystem *mesgsys, void **user_data);
 void process_camera_constraint(LLMessageSystem *mesgsys, void **user_data);
diff --git a/indra/newview/llviewerprecompiledheaders.h b/indra/newview/llviewerprecompiledheaders.h
index 0669f34c2f04a742efa44eeb3a1ba7ec2cee7945..0e0265d30b3ce84e8d7c205e6466403f616b3053 100644
--- a/indra/newview/llviewerprecompiledheaders.h
+++ b/indra/newview/llviewerprecompiledheaders.h
@@ -180,7 +180,6 @@
 #include "llinstantmessage.h"
 #include "llinvite.h"
 //#include "llloginflags.h"
-#include "lllogtextmessage.h"
 #include "llmail.h"
 #include "llmessagethrottle.h"
 #include "llnamevalue.h"
diff --git a/indra/test/llservicebuilder_tut.cpp b/indra/test/llservicebuilder_tut.cpp
index 127a2a12205f8585e5d4ed6dc89b51995a02d342..14f3774f7c7587c3628395a90bc4efbfb1082f45 100644
--- a/indra/test/llservicebuilder_tut.cpp
+++ b/indra/test/llservicebuilder_tut.cpp
@@ -113,5 +113,66 @@ namespace tut
 			test_url ,
 			"/proc/do/something/useful?estate_id=1&query=public");
 	}
+
+	template<> template<>
+	void ServiceBuilderTestObject::test<6>()
+	{
+		LLSD test_block;
+		test_block["service-builder"] = "Which way to the {${$baz}}?";
+		mServiceBuilder.createServiceDefinition(
+			"ServiceBuilderTest",
+			test_block["service-builder"]);	
+
+		LLSD data_map;
+		data_map["foo"] = "bar";
+		data_map["baz"] = "foo";
+		std::string test_url = mServiceBuilder.buildServiceURI(
+			"ServiceBuilderTest",
+			data_map);
+		ensure_equals(
+			"recursive url creation",
+			test_url ,
+			"Which way to the bar?");
+	}
+
+	template<> template<>
+	void ServiceBuilderTestObject::test<7>()
+	{
+		LLSD test_block;
+		test_block["service-builder"] = "Which way to the {$foo}?";
+		mServiceBuilder.createServiceDefinition(
+			"ServiceBuilderTest",
+			test_block["service-builder"]);	
+
+		LLSD data_map;
+		data_map["baz"] = "foo";
+		std::string test_url = mServiceBuilder.buildServiceURI(
+			"ServiceBuilderTest",
+			data_map);
+		ensure_equals(
+			"fails to do replacement",
+			test_url ,
+			"Which way to the {$foo}?");
+	}
+
+	template<> template<>
+	void ServiceBuilderTestObject::test<8>()
+	{
+		LLSD test_block;
+		test_block["service-builder"] = "/proc/{$proc}{%params}";
+		mServiceBuilder.createServiceDefinition(
+			"ServiceBuilderTest",
+			test_block["service-builder"]);	
+		LLSD data_map;
+		data_map["proc"] = "do/something/useful";
+		data_map["params"] = LLSD();
+		std::string test_url = mServiceBuilder.buildServiceURI(
+			"ServiceBuilderTest",
+			data_map);
+		ensure_equals(
+			"strip params",
+			test_url ,
+			"/proc/do/something/useful");
+	}
 }