diff --git a/applications/gui2/src/modules/camera.cpp b/applications/gui2/src/modules/camera.cpp
index 5eeb063fb32949b251fee833ecacfa09830623fd..4ef92ceba59b95241dda851242ccbbea2e46e957 100644
--- a/applications/gui2/src/modules/camera.cpp
+++ b/applications/gui2/src/modules/camera.cpp
@@ -11,7 +11,7 @@ using ftl::gui2::Camera;
 using ftl::codecs::Channel;
 using ftl::rgbd::Capability;
 using namespace std::literals::chrono_literals;
-using ftl::data::MessageType;
+using ftl::data::Message;
 
 void Camera::init() {
 
@@ -83,7 +83,7 @@ void Camera::update(double delta) {
 				else jmeta.erase("VR");
 			}
 
-			std::list<ftl::data::Message> messages;
+			std::map<ftl::data::Message,std::string> messages;
 			{
 				UNIQUE_LOCK(mtx_, lk);
 				std::swap(messages, messages_);
@@ -94,12 +94,12 @@ void Camera::update(double delta) {
 			if (messages.size() > 0) {
 				for (const auto &m : messages) {
 					auto &data = jmsgs.emplace_back();
-					data["value"] = m.message;
+					data["value"] = m.second;
 					data["nokey"] = true;
-					if (m.type == MessageType::MSG_ERROR) {
+					if (int(m.first) < 1024) {
 						data["icon"] = ENTYPO_ICON_WARNING;
 						data["colour"] = "#0000ff";
-					} else if (m.type == MessageType::MSG_WARNING) {
+					} else if (int(m.first) < 2046) {
 						data["icon"] = ENTYPO_ICON_WARNING;
 						data["colour"] = "#00a6f0";
 					} else {
@@ -250,12 +250,12 @@ void Camera::activate(ftl::data::FrameID id) {
 
 			// Extract and record any frame messages
 			auto &frame = fs->frames[frame_idx];
-			if (frame.has(Channel::Messages)) {
-				const auto &msgs = frame.get<std::list<ftl::data::Message>>(Channel::Messages);
+			if (frame.hasMessages()) {
+				const auto &msgs = frame.messages();
 				//auto &jmsgs = mod->getJSON(StatisticsPanel::LOGGING);
 
 				UNIQUE_LOCK(mtx_, lk);
-				messages_.insert(messages_.end(), msgs.begin(), msgs.end());
+				messages_.insert(msgs.begin(), msgs.end());
 			}
 
 			// Some capabilities can change over time
diff --git a/applications/gui2/src/modules/camera.hpp b/applications/gui2/src/modules/camera.hpp
index 8e484dc9daadb5beeef438c037aea7e420ad6398..16bf9f1b44751382d911ecac0aba66f8524122dd 100644
--- a/applications/gui2/src/modules/camera.hpp
+++ b/applications/gui2/src/modules/camera.hpp
@@ -84,7 +84,7 @@ private:
 	std::unique_ptr<ftl::render::Colouriser> colouriser_;
 	std::unique_ptr<ftl::overlay::Overlay> overlay_;
 
-	std::list<ftl::data::Message> messages_;
+	std::map<ftl::data::Message,std::string> messages_;
 
 	CameraView* view = nullptr;
 
diff --git a/applications/gui2/src/views/statistics.cpp b/applications/gui2/src/views/statistics.cpp
index a593ac942c9506ebba0b6b020ed54d2d36cbed34..926e7b6b449cf013ca1af74c76a5dea09333d954 100644
--- a/applications/gui2/src/views/statistics.cpp
+++ b/applications/gui2/src/views/statistics.cpp
@@ -15,7 +15,7 @@ using ftl::gui2::StatisticsWidget;
 using std::string;
 
 StatisticsWidget::StatisticsWidget(nanogui::Widget* parent, ftl::gui2::Statistics* ctrl) :
-		nanogui::Widget(parent), ctrl_(ctrl), last_stats_count_(0) {
+		nanogui::Window(parent,""), ctrl_(ctrl), last_stats_count_(0) {
 
 	setWidth(200);
 }
diff --git a/applications/gui2/src/views/statistics.hpp b/applications/gui2/src/views/statistics.hpp
index 639282a490aa8ba2fdb4c96a74b769be565ef7d1..a6b0b75350a549fc03d28c7e055365c9e1c98f27 100644
--- a/applications/gui2/src/views/statistics.hpp
+++ b/applications/gui2/src/views/statistics.hpp
@@ -1,6 +1,7 @@
 #pragma once
 
 #include <nanogui/widget.h>
+#include <nanogui/window.h>
 
 namespace ftl
 {
@@ -9,7 +10,7 @@ namespace gui2
 
 class Statistics;
 
-class StatisticsWidget : public nanogui::Widget {
+class StatisticsWidget : public nanogui::Window {
 public:
 	StatisticsWidget(nanogui::Widget *parent, Statistics* ctrl);
 	virtual void draw(NVGcontext *ctx);
diff --git a/applications/vision/src/main.cpp b/applications/vision/src/main.cpp
index c2860bf5c276d8005e2b55e2210a3439d5cc61aa..ca418497fe66be307b2dff18a838a7aac0a21c3f 100644
--- a/applications/vision/src/main.cpp
+++ b/applications/vision/src/main.cpp
@@ -196,7 +196,7 @@ static void run(ftl::Configurable *root) {
 	auto h = creator->onFrameSet([sender,outstream,&stats_count,&latency,&frames,&stats_time,pipeline,&busy,&encodable](const ftl::data::FrameSetPtr &fs) {
 		if (busy) {
 			LOG(WARNING) << "Depth pipeline drop: " << fs->timestamp();
-			fs->firstFrame().warning("Depth pipeline drop");
+			fs->firstFrame().message(ftl::data::Message::WARNING_PIPELINE_DROP, "Depth pipeline drop");
 			return true;
 		}
 		busy = true;
diff --git a/components/operators/src/colours.cpp b/components/operators/src/colours.cpp
index 73436417770219b758fb60752e75f1bb52857e65..1e8b9a6245725b317d51b592784af2b835b04efe 100644
--- a/components/operators/src/colours.cpp
+++ b/components/operators/src/colours.cpp
@@ -16,7 +16,7 @@ ColourChannels::~ColourChannels() {
 
 bool ColourChannels::apply(ftl::rgbd::Frame &in, ftl::rgbd::Frame &out, cudaStream_t stream) {
 	if (!in.hasChannel(Channel::Colour)) {
-		in.warning("No colour channel found");
+		in.message(ftl::data::Message::WARNING_MISSING_CHANNEL, "No colour channel found");
 		return false;
 	}
 
diff --git a/components/operators/src/operator.cpp b/components/operators/src/operator.cpp
index 339ba27f81bb32f05c54fe66df0ded78792ca566..4d6c43cd65d720bb4038e56242af284d8750acd2 100644
--- a/components/operators/src/operator.cpp
+++ b/components/operators/src/operator.cpp
@@ -83,7 +83,7 @@ bool Graph::apply(FrameSet &in, FrameSet &out, cudaStream_t stream) {
 						instance->apply(in.frames[j].cast<ftl::rgbd::Frame>(), out.frames[j].cast<ftl::rgbd::Frame>(), stream_actual);
 					} catch (const std::exception &e) {
 						LOG(ERROR) << "Operator exception for '" << instance->config()->getID() << "': " << e.what();
-						in.frames[j].error("Operator exception");
+						in.frames[j].message(ftl::data::Message::ERROR_OPERATOR_EXCEPTION, "Operator exception");
 						success = false;
 						break;
 					}
@@ -98,7 +98,7 @@ bool Graph::apply(FrameSet &in, FrameSet &out, cudaStream_t stream) {
 					instance->apply(in, out, stream_actual);
 				} catch (const std::exception &e) {
 					LOG(ERROR) << "Operator exception for '" << instance->config()->getID() << "': " << e.what();
-					if (in.frames.size() > 0) in.frames[0].error("Operator exception");
+					if (in.frames.size() > 0) in.frames[0].message(ftl::data::Message::ERROR_OPERATOR_EXCEPTION, "Operator exception");
 					success = false;
 					break;
 				}
@@ -149,6 +149,7 @@ bool Graph::apply(Frame &in, Frame &out, cudaStream_t stream) {
 			} catch (const std::exception &e) {
 				LOG(ERROR) << "Operator exception for '" << instance->config()->getID() << "': " << e.what();
 				success = false;
+				out.message(ftl::data::Message::ERROR_OPERATOR_EXCEPTION, "Operator exception");
 				break;
 			}
 		}
diff --git a/components/streams/src/builder.cpp b/components/streams/src/builder.cpp
index 7dc8400c239c672c2eb287c090026a9887aa6aee..f5fbd18cf397dfa0ada58ba785727bad7160097b 100644
--- a/components/streams/src/builder.cpp
+++ b/components/streams/src/builder.cpp
@@ -139,7 +139,7 @@ void IntervalSourceBuilder::start() {
 		for (auto *s : srcs_) {
 			if (!s->retrieve(fs->firstFrame())) {
 				LOG(WARNING) << "Frame is being skipped: " << ts;
-				fs->firstFrame().warning("Frame is being skipped");
+				fs->firstFrame().message(ftl::data::Message::WARNING_FRAME_DROP, "Frame is being skipped");
 			}
 		}
 
diff --git a/components/structures/include/ftl/data/messages.hpp b/components/structures/include/ftl/data/messages.hpp
index 13fcc870b4abe5bf5cf3e3be373e0694e8b75d0c..de7e42e1f45067bb2f955ed6cfe4457b8f01c5db 100644
--- a/components/structures/include/ftl/data/messages.hpp
+++ b/components/structures/include/ftl/data/messages.hpp
@@ -6,24 +6,21 @@
 namespace ftl {
 namespace data {
 
-enum class MessageType : int {
-	MSG_INFORMATION=0,
-	MSG_WARNING,
-	MSG_ERROR,
-	CHAT
-};
-
-struct Message {
-	MessageType type;
-	int authorID;
-	std::string message;
-
-	MSGPACK_DEFINE(type, authorID, message);
+enum class Message : int {
+	ERROR_UNKNOWN = 0,
+	ERROR_OPERATOR_EXCEPTION,
+	ERROR_FRAME_GRAB,
+	WARNING_UNKNOWN = 1024,
+	WARNING_FRAME_DROP,
+	WARNING_PIPELINE_DROP,
+	WARNING_MISSING_CHANNEL,
+	INFORMATION_UNKNOWN = 2046,
+	OTHER_UNKNOWN = 3072
 };
 
 }
 }
 
-MSGPACK_ADD_ENUM(ftl::data::MessageType);
+MSGPACK_ADD_ENUM(ftl::data::Message);
 
 #endif
\ No newline at end of file
diff --git a/components/structures/include/ftl/data/new_frame.hpp b/components/structures/include/ftl/data/new_frame.hpp
index c961418effaf48930ed47b752691698e5e36830f..c17ec539fe72487ecf1572811a738e6e2715457c 100644
--- a/components/structures/include/ftl/data/new_frame.hpp
+++ b/components/structures/include/ftl/data/new_frame.hpp
@@ -631,16 +631,14 @@ class Frame {
 
 	// ==== Wrapper functions ==================================================
 
-	void message(ftl::data::MessageType type, int id, const std::string &msg);
+	void message(ftl::data::Message code, const std::string &msg);
 
-	void error(const std::string &msg);
-	void error(const ftl::Formatter &msg);
+	void message(ftl::data::Message code, const ftl::Formatter &msg);
 
-	void warning(const std::string &msg);
-	void warning(const ftl::Formatter &msg);
+	/** Note, throws exception if `Channel::Messages` is missing. */
+	const std::map<ftl::data::Message,std::string> &messages() const;
 
-	void information(const std::string &msg);
-	void information(const ftl::Formatter &msg);
+	inline bool hasMessages() const { return hasChannel(ftl::codecs::Channel::Messages); }
 
 	/**
 	 * Get or generate a name for this frame.
diff --git a/components/structures/src/new_frame.cpp b/components/structures/src/new_frame.cpp
index 487b6468c5a777c155ffc10cc00ccca74e09400e..d2b319d5969c9b66ae74932fd4218ab8fdbcedd0 100644
--- a/components/structures/src/new_frame.cpp
+++ b/components/structures/src/new_frame.cpp
@@ -8,7 +8,7 @@ using ftl::data::ChannelConfig;
 using ftl::data::StorageMode;
 using ftl::data::FrameStatus;
 using ftl::codecs::Channel;
-using ftl::data::MessageType;
+using ftl::data::Message;
 
 #define LOGURU_REPLACE_GLOG 1
 #include <loguru.hpp>
@@ -437,37 +437,17 @@ std::unordered_set<ftl::codecs::Channel> Frame::allChannels() const {
 	return res;
 }
 
-void Frame::message(ftl::data::MessageType type, int id, const std::string &msg) {
-	auto msgs = create<std::list<ftl::data::Message>>(Channel::Messages);
-	ftl::data::Message msgdata;
-	msgdata.authorID = id;
-	msgdata.type = type;
-	msgdata.message = msg;
-	msgs = msgdata;
+const std::map<ftl::data::Message,std::string> &Frame::messages() const {
+	return get<std::map<ftl::data::Message,std::string>>(Channel::Messages);
 }
 
-void Frame::error(const std::string &msg) {
-	message(MessageType::MSG_ERROR, -1, msg);
+void Frame::message(ftl::data::Message code, const std::string &msg) {
+	auto &msgs = create<std::map<ftl::data::Message,std::string>>(Channel::Messages);
+	msgs[code] = msg;
 }
 
-void Frame::error(const ftl::Formatter &msg) {
-	error(msg.str());
-}
-
-void Frame::warning(const std::string &msg) {
-	message(MessageType::MSG_WARNING, -1, msg);
-}
-
-void Frame::warning(const ftl::Formatter &msg) {
-	warning(msg.str());
-}
-
-void Frame::information(const std::string &msg) {
-	message(MessageType::MSG_INFORMATION, -1, msg);
-}
-
-void Frame::information(const ftl::Formatter &msg) {
-	information(msg.str());
+void Frame::message(ftl::data::Message code, const ftl::Formatter &msg) {
+	message(code, msg.str());
 }
 
 std::string Frame::name() const {