diff --git a/include/class.orm.php b/include/class.orm.php
index 7c715f43a19002507cdf9ecbbb8ba8cb64ee8836..d8e70f6007b086fe77ab20ab0a79da96c97780eb 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -1105,7 +1105,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
         // Load defaults from model
         $model = $this->model;
         $query = clone $this;
-        if (!$query->ordering && isset($model::$meta['ordering']))
+        if (!$options['nosort'] && !$query->ordering && isset($model::$meta['ordering']))
             $query->ordering = $model::$meta['ordering'];
         if (false !== $query->related && !$query->values && $model::$meta['select_related'])
             $query->related = $model::$meta['select_related'];
@@ -1311,6 +1311,12 @@ class ModelInstanceManager extends ResultSet {
             if ($cache)
                 $this->cache($m);
         }
+        elseif (get_class($m) != $modelClass) {
+            // Change the class of the object to be returned to match what
+            // was expected
+            // TODO: Emit a warning?
+            $m = new $modelClass($m->ht);
+        }
         // Wrap annotations in an AnnotatedModel
         if ($extras) {
             $m = new AnnotatedModel($m, $extras);
@@ -1904,7 +1910,7 @@ class MySqlCompiler extends SqlCompiler {
     function __in($a, $b) {
         if (is_array($b)) {
             $vals = array_map(array($this, 'input'), $b);
-            $b = implode(', ', $vals);
+            $b = '('.implode(', ', $vals).')';
         }
         // MySQL doesn't support LIMIT or OFFSET in subqueries. Instead, add
         // the query as a JOIN and add the join constraint into the WHERE
@@ -1918,7 +1924,7 @@ class MySqlCompiler extends SqlCompiler {
         else {
             $b = $this->input($b);
         }
-        return sprintf('%s IN (%s)', $a, $b);
+        return sprintf('%s IN %s', $a, $b);
     }
 
     function __isnull($a, $b) {
@@ -2009,7 +2015,7 @@ class MySqlCompiler extends SqlCompiler {
         if ($what instanceof QuerySet) {
             $q = $what->getQuery(array('nosort'=>true));
             $this->params = array_merge($this->params, $q->params);
-            return $q->sql;
+            return '('.$q->sql.')';
         }
         elseif ($what instanceof SqlFunction) {
             return $what->toSql($this);
@@ -2079,7 +2085,7 @@ class MySqlCompiler extends SqlCompiler {
 
         // Compile the ORDER BY clause
         $sort = '';
-        if (($columns = $queryset->getSortFields()) && !isset($this->options['nosort'])) {
+        if ($columns = $queryset->getSortFields()) {
             $orders = array();
             foreach ($columns as $sort) {
                 $dir = 'ASC';
@@ -2106,6 +2112,7 @@ class MySqlCompiler extends SqlCompiler {
         // Compile the field listing
         $fields = array();
         $group_by = array();
+        $model::_inspect();
         $table = $this->quote($model::$meta['table']).' '.$rootAlias;
         // Handle related tables
         if ($queryset->related) {
@@ -2113,7 +2120,6 @@ class MySqlCompiler extends SqlCompiler {
             $fieldMap = $theseFields = array();
             $defer = $queryset->defer ?: array();
             // Add local fields first
-            $model::_inspect();
             foreach ($model::$meta['fields'] as $f) {
                 // Handle deferreds
                 if (isset($defer[$f]))
diff --git a/include/class.search.php b/include/class.search.php
index 137c1d375b041492ba8d285c6d2c27e792195867..bfc59f4c07101a850e91f92ba983457d4e44de88 100644
--- a/include/class.search.php
+++ b/include/class.search.php
@@ -450,7 +450,8 @@ class MysqlSearchBackend extends SearchBackend {
             return false;
 
         while ($row = db_fetch_row($res)) {
-            $ticket = Ticket::lookup($row[0]);
+            if (!($ticket = Ticket::lookup($row[0])))
+                continue;
             $cdata = $ticket->loadDynamicData();
             $content = array();
             foreach ($cdata as $k=>$a)
diff --git a/include/class.thread.php b/include/class.thread.php
index e5a78cde2258b6980add5bd495b766ad5a4dbbd1..2b8be9fa597ec0ecbdcef95a24088f04a8cfd2e7 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -1553,6 +1553,10 @@ class ThreadEvent extends VerySimpleModel {
                 $collabs = array();
                 if ($data['add']) {
                     foreach ($data['add'] as $c) {
+                        if (is_array($c))
+                            $c = sprintf(__("%s via %a"
+                                /* e.g. "Me <me@company.me> via Email (to)" */),
+                                $c[0], $c[1]);
                         $collabs[] = Format::htmlchars($c);
                     }
                 }
@@ -1666,34 +1670,31 @@ class ThreadEvent extends VerySimpleModel {
         include $inc . 'templates/thread-event.tmpl.php';
     }
 
-    static function create($ht=false) {
+    static function create($ht=false, $user=false) {
         $inst = parent::create($ht);
         $inst->timestamp = SqlFunction::NOW();
 
         global $thisstaff, $thisclient;
-        if ($thisstaff) {
+        $user = is_object($user) ? $user : $thisstaff ?: $thisclient;
+        if ($user instanceof Staff) {
             $inst->uid_type = 'S';
-            $inst->uid = $thisstaff->getId();
+            $inst->uid = $user->getId();
         }
-        else if ($thisclient) {
+        elseif ($user instanceof User) {
             $inst->uid_type = 'U';
-            $inst->uid = $thisclient->getId();
+            $inst->uid = $user->getId();
         }
 
         return $inst;
     }
 
-    static function forTicket($ticket, $state) {
+    static function forTicket($ticket, $state, $user=false) {
         $inst = static::create(array(
             'staff_id' => $ticket->getStaffId(),
             'team_id' => $ticket->getTeamId(),
             'dept_id' => $ticket->getDeptId(),
             'topic_id' => $ticket->getTopicId(),
-        ));
-        if (!isset($inst->uid_type) && $state == self::CREATED) {
-            $inst->uid_type = 'U';
-            $inst->uid = $ticket->getOwnerId();
-        }
+        ), $user);
         return $inst;
     }
 }
@@ -1705,13 +1706,27 @@ class ThreadEvents extends InstrumentedList {
             ->update(array('annulled' => 1));
     }
 
-    function log($object, $state, $data=null, $annul=null, $username=null) {
+    /**
+     * Add an event to the thread activity log.
+     *
+     * Parameters:
+     * $object - Object to log activity for
+     * $state - State name of the activity (one of 'created', 'edited',
+     *      'deleted', 'closed', 'reopened', 'error', 'collab', 'resent',
+     *      'assigned', 'transferred')
+     * $data - (array?) Details about the state change
+     * $user - (string|User|Staff) user triggering the state change
+     * $annul - (state) a corresponding state change that is annulled by
+     *      this event
+     */
+    function log($object, $state, $data=null, $user=null, $annul=null) {
         global $thisstaff, $thisclient;
 
         if ($object instanceof Ticket)
-            $event = ThreadEvent::forTicket($object, $state);
+            // TODO: Use $object->createEvent()
+            $event = ThreadEvent::forTicket($object, $state, $user);
         else
-            $event = ThreadEvent::create();
+            $event = ThreadEvent::create(false, $user);
 
         # Annul previous entries if requested (for instance, reopening a
         # ticket will annul an 'closed' entry). This will be useful to
@@ -1720,11 +1735,14 @@ class ThreadEvents extends InstrumentedList {
             $this->annul($annul);
         }
 
-        if ($username === null) {
-            if ($thisstaff) {
-                $username = $thisstaff->getUserName();
+        $username = $user;
+        $user = is_object($user) ? $user : $thisclient ?: $thisstaff;
+        if (!is_string($username)) {
+            if ($user instanceof Staff) {
+                $username = $user->getUserName();
             }
-            else if ($thisclient) {
+            // XXX: Use $user here
+            elseif ($thisclient) {
                 if ($thisclient->hasAccount)
                     $username = $thisclient->getAccount()->getUserName();
                 if (!$username)
@@ -2097,6 +2115,7 @@ implements TemplateVariable {
 
     static $types = array(
         ObjectModel::OBJECT_TYPE_TASK => 'TaskThread',
+        ObjectModel::OBJECT_TYPE_TICKET => 'TicketThread',
     );
 
     var $counts;
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 3a48f7db8f3249d0cff914c4c15acbf2270fc726..8a771c3237eb81f0333b7af69855f358432bd54e 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -647,20 +647,20 @@ implements RestrictedAccess, Threadable {
     }
 
     function getLastRespondent() {
-
         if (!isset($this->lastrespondent)) {
             $this->lastresponent = Staff::objects()
                 ->filter(array(
                 'staff_id' => static::objects()
                     ->filter(array(
-                        'thread__entry__type' => 'R',
-                        'thread__entry__staff_id__gt' => 0
+                        'thread__entries__type' => 'R',
+                        'thread__entries__staff_id__gt' => 0
                     ))
-                    ->values_flat('thread__entry__staff_id')
-                    ->order_by('-thread__entry__id')
-                    ->first()
+                    ->values_flat('thread__entries__staff_id')
+                    ->order_by('-thread__entries__id')
+                    ->limit(1)
                 ))
-                ->first();
+                ->first()
+                ?: false;
         }
         return $this->lastrespondent;
     }
@@ -799,7 +799,7 @@ implements RestrictedAccess, Threadable {
         return $fields[0];
     }
 
-    function addCollaborator($user, $vars, &$errors) {
+    function addCollaborator($user, $vars, &$errors, $event=true) {
 
         if (!$user || $user->getId() == $this->getOwnerId())
             return null;
@@ -813,7 +813,8 @@ implements RestrictedAccess, Threadable {
         $this->collaborators = null;
         $this->recipients = null;
 
-        $this->logEvent('collab', array('add' => array($c->toString())));
+        if ($event)
+            $this->logEvent('collab', array('add' => array($c->toString())));
 
         return $c;
     }
@@ -1020,9 +1021,9 @@ implements RestrictedAccess, Threadable {
         if ($this->getStatusId() == $status->getId())
             return true;
 
-        //TODO: move this up.
+        // Perform checks on the *new* status, _before_ the status changes
         $ecb = null;
-        switch($status->getState()) {
+        switch ($status->getState()) {
             case 'closed':
                 if ($this->getMissingRequiredFields()) {
                     $errors['err'] = sprintf(__(
@@ -1034,7 +1035,7 @@ implements RestrictedAccess, Threadable {
                 $this->duedate = null;
                 if ($thisstaff && $set_closing_agent)
                     $this->staff = $thisstaff;
-                $this->clearOverdue();
+                $this->clearOverdue(false);
 
                 $ecb = function($t) {
                     $t->logEvent('closed');
@@ -1046,7 +1047,7 @@ implements RestrictedAccess, Threadable {
                 if ($this->isClosed()) {
                     $this->closed = $this->lastupdate = $this->reopened = SqlFunction::NOW();
                     $ecb = function ($t) {
-                        $t->logEvent('reopened', false, 'closed');
+                        $t->logEvent('reopened', false, null, 'closed');
                     };
                 }
 
@@ -1066,7 +1067,7 @@ implements RestrictedAccess, Threadable {
         // Log status change b4 reload — if currently has a status. (On new
         // ticket, the ticket is opened and thereafter the status is set to
         // the requested status).
-        if ($current_status = $this->getStatus()) {
+        if ($hadStatus) {
             $alert = false;
             if ($comments) {
                 // Send out alerts if comments are included
@@ -1782,7 +1783,7 @@ implements RestrictedAccess, Threadable {
         return true;
     }
 
-    function clearOverdue() {
+    function clearOverdue($save=true) {
         if (!$this->isOverdue())
             return true;
 
@@ -1798,7 +1799,7 @@ implements RestrictedAccess, Threadable {
         if ($this->getSLADueDate() && Misc::db2gmtime($this->getSLADueDate()) <= Misc::gmtime())
             $this->sla = null;
 
-        return $this->save();
+        return $save ? $this->save() : true;
     }
 
     //Dept Tranfer...with alert.. done by staff
@@ -2066,14 +2067,14 @@ implements RestrictedAccess, Threadable {
                     continue;
 
                 if (($user=User::fromVars($recipient)))
-                    if ($c=$this->addCollaborator($user, $info, $errors))
+                    if ($c=$this->addCollaborator($user, $info, $errors, false))
                         // FIXME: This feels very unwise — should be a
                         // string indexed array for future
                         $collabs[] = array((string)$c, $recipient['source']);
             }
             // TODO: Can collaborators add others?
             if ($collabs) {
-                $this->logEvent('collab', array('add' => $collabs));
+                $this->logEvent('collab', array('add' => $collabs), $message->user);
             }
         }
 
@@ -2310,8 +2311,8 @@ implements RestrictedAccess, Threadable {
     }
 
     // History log -- used for statistics generation (pretty reports)
-    function logEvent($state, $data=null, $annul=null, $staff=null) {
-        $this->getThread()->getEvents()->log($this, $state, $data, $annul, $staff);
+    function logEvent($state, $data=null, $user=null, $annul=null) {
+        $this->getThread()->getEvents()->log($this, $state, $data, $user, $annul);
     }
 
     //Insert Internal Notes
@@ -2566,11 +2567,11 @@ implements RestrictedAccess, Threadable {
         }
 
         Signal::send('model.updated', $this);
-        return true;
+        return $this->save();
     }
 
    /*============== Static functions. Use Ticket::function(params); =============nolint*/
-    function getIdByNumber($number, $email=null, $ticket=false) {
+    static function getIdByNumber($number, $email=null, $ticket=false) {
 
         if (!$number)
             return 0;
@@ -2592,8 +2593,8 @@ implements RestrictedAccess, Threadable {
         }
     }
 
-    function lookupByNumber($number, $email=null) {
-        return self::getIdByNumber($number, $email, true);
+    static function lookupByNumber($number, $email=null) {
+        return static::getIdByNumber($number, $email, true);
     }
 
     static function isTicketNumberUnique($number) {
@@ -3105,6 +3106,9 @@ implements RestrictedAccess, Threadable {
 
         $dept = $ticket->getDept();
 
+        // Start tracking ticket lifecycle events (created should come first!)
+        $ticket->logEvent('created', null, $thisstaff ?: $user);
+
         // Add organizational collaborators
         if ($org && $org->autoAddCollabs()) {
             $pris = $org->autoAddPrimaryContactsAsCollabs();
@@ -3148,11 +3152,10 @@ implements RestrictedAccess, Threadable {
             // Auto assign staff or team - auto assignment based on filter
             // rules. Both team and staff can be assigned
             if ($vars['staffId'])
-                 $ticket->assignToStaff($vars['staffId'], _S('Auto Assignment'));
+                 $ticket->assignToStaff($vars['staffId']);
             if ($vars['teamId'])
                 // No team alert if also assigned to an individual agent
-                $ticket->assignToTeam($vars['teamId'], _S('Auto Assignment'),
-                    !$vars['staffId']);
+                $ticket->assignToTeam($vars['teamId'], false, !$vars['staffId']);
         }
 
         // Update the estimated due date in the database
@@ -3208,9 +3211,6 @@ implements RestrictedAccess, Threadable {
             $ticket->onOpenLimit($autorespond && strcasecmp($origin, 'staff'));
         }
 
-        /* Start tracking ticket lifecycle events */
-        $ticket->logEvent('created');
-
         // Fire post-create signal (for extra email sending, searching)
         Signal::send('ticket.created', $ticket);
 
diff --git a/include/class.user.php b/include/class.user.php
index 655edfd00e4dcb9cd6f6023018a1192ff54c02c6..3ba2b54ae1d3cb0d7f194036d1a5412199b1afd4 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -51,7 +51,7 @@ class UserModel extends VerySimpleModel {
             'account' => array(
                 'list' => false,
                 'null' => true,
-                'reverse' => 'UserAccount.user',
+                'reverse' => 'ClientAccount.user',
             ),
             'org' => array(
                 'null' => true,
diff --git a/include/client/templates/thread-entries.tmpl.php b/include/client/templates/thread-entries.tmpl.php
index d031182cad2fed9947e5bf75a953b05fb81b0374..9b1fc704db6ed350c06b932abd3fcb93bcf00bf6 100644
--- a/include/client/templates/thread-entries.tmpl.php
+++ b/include/client/templates/thread-entries.tmpl.php
@@ -25,7 +25,7 @@ if (count($entries)) {
         //       changes in dates between thread items.
         foreach ($entries as $entry) {
             // Emit all events prior to this entry
-            while ($event && $event->timestamp <= $entry->created) {
+            while ($event && $event->timestamp < $entry->created) {
                 $event->render(ThreadEvent::MODE_CLIENT);
                 $events->next();
                 $event = $events->current();
diff --git a/include/client/templates/thread-entry.tmpl.php b/include/client/templates/thread-entry.tmpl.php
index 70e4bbb5c6fae24ba42a48a2c67520b044d03e58..6c16c0660d09c5e71527c90cffd302b0731df65a 100644
--- a/include/client/templates/thread-entry.tmpl.php
+++ b/include/client/templates/thread-entry.tmpl.php
@@ -57,6 +57,7 @@ if ($user && ($url = $user->get_gravatar(48)))
     </div>
     <div class="thread-body" id="thread-id-<?php echo $entry->getId(); ?>">
         <div><?php echo $entry->getBody()->toHtml(); ?></div>
+        <div class="clear"></div>
 <?php
     if ($entry->has_attachments) { ?>
     <div class="attachments"><?php
diff --git a/include/staff/templates/thread-entry.tmpl.php b/include/staff/templates/thread-entry.tmpl.php
index cc5a704ab300058b15959a048dc0112521e63728..e7c31e2a512eb6eb614b9e88910dafc3a0a32107 100644
--- a/include/staff/templates/thread-entry.tmpl.php
+++ b/include/staff/templates/thread-entry.tmpl.php
@@ -45,7 +45,8 @@ if ($user && ($url = $user->get_gravatar(48)))
         </div>
 <?php
         echo sprintf(__('<b>%s</b> posted %s'), $name,
-            sprintf('<time class="relative" datetime="%s" title="%s">%s</time>',
+            sprintf('<a name="entry-%d" href="#entry-%1$s"><time class="relative" datetime="%s" title="%s">%s</time></a>',
+                $entry->id,
                 date(DateTime::W3C, Misc::db2gmtime($entry->created)),
                 Format::daydatetime($entry->created),
                 Format::relativeTime(Misc::db2gmtime($entry->created))
@@ -57,6 +58,7 @@ if ($user && ($url = $user->get_gravatar(48)))
     </div>
     <div class="thread-body" id="thread-id-<?php echo $entry->getId(); ?>">
         <div><?php echo $entry->getBody()->toHtml(); ?></div>
+        <div class="clear"></div>
 <?php
     if ($entry->has_attachments) { ?>
     <div class="attachments"><?php
diff --git a/include/upgrader/streams/core/9143a511-0d6099a6.patch.sql b/include/upgrader/streams/core/9143a511-0d6099a6.patch.sql
index 401b6780cfd4680deea77c8efa1a935f5184a5a3..c6bf19b42ba79cfde7f076d9b7b056856d0a0b55 100644
--- a/include/upgrader/streams/core/9143a511-0d6099a6.patch.sql
+++ b/include/upgrader/streams/core/9143a511-0d6099a6.patch.sql
@@ -22,7 +22,7 @@ CREATE TABLE `%TABLE_PREFIX%_ticket_thread_evt`
     WHERE `object_type` = 'T';
 
 UPDATE `%TABLE_PREFIX%thread_event` A1
-    JOIN `%TABLE_PREFIX%_ticket_thread_evt` A2 ON (A1.`thread_id` = A2.`object_id`)
+    LEFT JOIN `%TABLE_PREFIX%_ticket_thread_evt` A2 ON (A1.`thread_id` = A2.`object_id`)
     SET A1.`thread_id` = A2.`id`;
 
 DROP TABLE `%TABLE_PREFIX%_ticket_thread_evt`;
diff --git a/scp/css/scp.css b/scp/css/scp.css
index c965cb7a97cc4a025750ed740c6e0123b532ff97..0181a06d0fa67ae762e1e1c779de278fb5ab1350 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -70,6 +70,9 @@ div#header a {
 time[title]:hover {
     text-decoration: underline;
 }
+a time.relative {
+    color: initial;
+}
 
 .small[class^="icon-"],
 .small[class*=" icon-"] {
diff --git a/scp/js/scp.js b/scp/js/scp.js
index 2d93705cdbbe88dfc41d3e5e57faf06e5f1b40a4..ef9fd3071ba409b70e3d42f710a589ed0fc26a28 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -1068,3 +1068,30 @@ function addSearchParam(key, value) {
     //this will reload the page, it's likely better to store this until finished
     return kvp.join('&');
 }
+
+// Periodically adjust relative times
+window.relativeAdjust = setInterval(function() {
+  var prettyDate = function(time) {
+    var date = new Date((time || "").replace(/-/g, "/").replace(/[TZ]/g, " ")),
+        diff = (((new Date()).getTime() - date.getTime()) / 1000),
+        day_diff = Math.floor(diff / 86400);
+
+    if (isNaN(day_diff) || day_diff < 0 || day_diff >= 31) return;
+
+    return day_diff == 0 && (
+         diff < 60 && __("just now")
+      || diff < 120 && __("about a minute ago")
+      || diff < 3600 && __("%d minutes ago").replace('%d', Math.floor(diff/60))
+      || diff < 7200 && __("about an hour ago")
+      || diff < 86400 &&  __("%d hours ago").replace('%d', Math.floor(diff/86400))
+    )
+    || day_diff == 1 && __("yesterday")
+    || day_diff < 7 && __("%d days ago").replace('%d', day_diff);
+    // Longer dates don't need to change dynamically
+  };
+  $('time.relative[datetime]').each(function() {
+    var rel = prettyDate($(this).attr('datetime'));
+    if (rel) $(this).text(rel);
+  });
+}, 20000);
+