chatSessionId === null || !in_array($rating, [-1, 0, 1], true) || !\Aiwend\Support\RequestId::isValid($requestId)) { return false; } return $this->db->transaction(function () use ($principal, $requestId, $rating): bool { $lock = $this->db->driver() === 'mysql' ? ' FOR UPDATE' : ''; // Same first row lock as message appending and erasure: no resurrection after deletion. $session = $this->db->selectOne('SELECT user_id, expires_at, revoked FROM {chat_sessions} WHERE site_id = ? AND id = ?' . $lock, [$principal->siteId, $principal->chatSessionId]); if ($session === null || (int) $session['revoked'] !== 0 || (int) $session['expires_at'] <= $this->clock->now() || ($session['user_id'] === null ? null : (int) $session['user_id']) !== $principal->userId) { return false; } $message = $this->db->selectOne("SELECT id FROM {messages} WHERE site_id = ? AND session_id = ? AND request_id = ? AND role = 'assistant' AND (outcome IS NULL OR outcome <> 'stopped')", [$principal->siteId, $principal->chatSessionId, $requestId]); if ($message === null) { return false; } $this->db->execute('DELETE FROM {message_feedback} WHERE site_id = ? AND message_id = ?', [$principal->siteId, $message['id']]); if ($rating !== 0) { $this->db->insert('message_feedback', ['site_id' => $principal->siteId, 'session_id' => $principal->chatSessionId, 'message_id' => $message['id'], 'rating' => $rating, 'updated_at' => $this->clock->now()]); } return true; }); } }