From a86bced1640ac1a695b95ac68b95e3fc581e687d Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Thu, 15 Oct 2026 12:57:51 +0200 Subject: refactor(chat): no thread_id — chat has no threads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing ever set it (the page and the CLI sent null) and nothing read it (the node's get_thread had no caller). It is no longer sent by the page or the CLI, in clear or in the sealed payload, nor relayed or served by the node; get_thread and its index are gone, and the protocol and design docs no longer name it. The node still accepts a chat_msg carrying one, from the desktop apps and CLIs already installed, and drops it. The column stays, empty, in chat.db: a node downgraded to an older version names it in every query. Co-Authored-By: Claude Opus 5.5 --- .../meshbay-node/src/meshbay_node/chat/store.py | 32 ++++++++-------------- packages/meshbay-node/src/meshbay_node/ops/chat.py | 1 - .../src/meshbay_node/transport/webrtc/chat.py | 20 +++++--------- 3 files changed, 19 insertions(+), 34 deletions(-) (limited to 'packages/meshbay-node/src') diff --git a/packages/meshbay-node/src/meshbay_node/chat/store.py b/packages/meshbay-node/src/meshbay_node/chat/store.py index 9e07aff..7610b33 100644 --- a/packages/meshbay-node/src/meshbay_node/chat/store.py +++ b/packages/meshbay-node/src/meshbay_node/chat/store.py @@ -54,8 +54,11 @@ CREATE TABLE IF NOT EXISTS messages ( sender_name TEXT DEFAULT '' ); CREATE INDEX IF NOT EXISTS idx_messages_ts ON messages(timestamp); -CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages(thread_id); """ +# `thread_id` is written and read by nothing: chat has no threads. The column +# stays, empty, because a node downgraded to an older version names it in every +# query, and its rows must keep opening. Its index goes; an older node makes it +# again on its own. # Additive, every one with a default, so an existing chat.db opens unchanged and # a node that is downgraded still reads its own rows. `CREATE TABLE IF NOT @@ -88,7 +91,6 @@ class StoredMessage: iteration: int payload: bytes timestamp: float - thread_id: str | None sender_name: str = "" format: int = FORMAT_PLAIN epoch: int = 0 @@ -97,16 +99,16 @@ class StoredMessage: sig: bytes | None = None -_COLUMNS = ("id, sender_id, iteration, payload, timestamp, thread_id, " +_COLUMNS = ("id, sender_id, iteration, payload, timestamp, " "sender_name, format, epoch, device, nonce, sig") def _row(r) -> StoredMessage: return StoredMessage( id=r[0], sender_id=r[1], iteration=r[2], payload=r[3], - timestamp=r[4], thread_id=r[5], sender_name=r[6] or "", - format=r[7] or FORMAT_PLAIN, epoch=r[8] or 0, - device=r[9], nonce=r[10], sig=r[11], + timestamp=r[4], sender_name=r[5] or "", + format=r[6] or FORMAT_PLAIN, epoch=r[7] or 0, + device=r[8], nonce=r[9], sig=r[10], ) @@ -132,6 +134,7 @@ class ChatStore: # nothing to show for it. pass await self._db.execute(_REPLAY_INDEX) + await self._db.execute("DROP INDEX IF EXISTS idx_messages_thread") await self._db.commit() async def close(self) -> None: @@ -151,7 +154,6 @@ class ChatStore: sender_id: str, iteration: int, payload: bytes, - thread_id: str | None = None, sender_name: str = "", *, format: int = FORMAT_PLAIN, @@ -171,9 +173,9 @@ class ChatStore: try: cursor = await self._db.execute( "INSERT INTO messages (sender_id, iteration, payload, timestamp, " - " thread_id, sender_name, format, epoch, device, nonce, sig) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - (sender_id, iteration, payload, ts, thread_id, sender_name, + " sender_name, format, epoch, device, nonce, sig) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + (sender_id, iteration, payload, ts, sender_name, format, epoch, device, nonce, sig), ) except aiosqlite.IntegrityError as e: @@ -234,16 +236,6 @@ class ChatStore: "SELECT 1 FROM messages WHERE id < ? LIMIT 1", (message_id,)) return await cursor.fetchone() is not None - async def get_thread(self, thread_id: str, limit: int = 100) -> list[StoredMessage]: - """Get messages in a thread.""" - cursor = await self._db.execute( - f"SELECT {_COLUMNS} " - "FROM messages WHERE thread_id = ? ORDER BY timestamp ASC LIMIT ?", - (thread_id, limit), - ) - rows = await cursor.fetchall() - return [_row(r) for r in rows] - @property def db_path(self) -> Path: """Where this store lives — the re-encryption command backs it up.""" diff --git a/packages/meshbay-node/src/meshbay_node/ops/chat.py b/packages/meshbay-node/src/meshbay_node/ops/chat.py index 9fc7b94..7caf85c 100644 --- a/packages/meshbay-node/src/meshbay_node/ops/chat.py +++ b/packages/meshbay-node/src/meshbay_node/ops/chat.py @@ -214,7 +214,6 @@ async def encrypt_chat_history(state: dict, group_id: str) -> dict: if isinstance(row.payload, bytes) else str(row.payload)) env = seal(key, group_id, epoch, device_b64, device_raw, sk_node, { "text": text, - "thread_id": row.thread_id, "sender_name": row.sender_name, "sent_at": int(row.timestamp), # The node sealed this after the fact; it did not witness it being diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py index ed2c43a..5bc9dbb 100644 --- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py +++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/chat.py @@ -75,7 +75,7 @@ _LINK_PREVIEW_RATE_NODE = 60 # C5a; this is the same question asked of the path nobody had asked it of. # # 64 KB of ciphertext is about sixty thousand characters. The sealed payload is -# the text, a thread id, a display name and a timestamp: an attachment is a file +# the text, a display name and a timestamp: an attachment is a file # on a root and travels as a reference (§4.5), so nothing legitimate comes close. MAX_CHAT_CIPHERTEXT = 64 * 1024 # Per account per group, not per connection: a second tab does not make a person @@ -330,18 +330,14 @@ class ChatMixin: # anyone on the node. gctx = self._group_ctx() chat_store = gctx.get("chat_store") - # The two fields that travel in clear beside the ciphertext (the sealed - # envelope carries its own). Stored and relayed to every member, so they - # are what they claim to be and no larger: a name as long as a username, - # a thread id as long as a message id. Anything else is dropped. + # The one field that travels in clear beside the ciphertext (the sealed + # envelope carries its own). Stored and relayed to every member, so it is + # what it claims to be and no larger: a name as long as a username. + # Anything else is dropped. A `thread_id` from an older client is + # ignored: chat has no threads. sender_name = msg.get("sender_name", "") if not isinstance(sender_name, str) or len(sender_name) > 64: sender_name = "" - thread_id = msg.get("thread_id") - id_like = (isinstance(thread_id, int) and not isinstance(thread_id, bool) - or isinstance(thread_id, str) and len(thread_id) <= 64) - if thread_id is not None and not id_like: - thread_id = None # Two shapes, and keeping them apart is what makes this deployable. # @@ -387,7 +383,7 @@ class ChatMixin: self._spawn(self._store_chat_message( chat_store, iteration=msg.get("iteration", 0), payload=raw, - thread_id=thread_id, sender_name=sender_name, + sender_name=sender_name, format=fmt, epoch=epoch, device=device, nonce=nonce, sig=sig, )) @@ -398,7 +394,6 @@ class ChatMixin: "sender_id": self._user_id, "sender_name": sender_name, "payload": payload, - "thread_id": thread_id, "timestamp": time.time(), "format": fmt, "epoch": epoch, @@ -613,7 +608,6 @@ class ChatMixin: "sender_id": m.sender_id, "sender_name": m.sender_name or names.get(m.sender_id, ""), "timestamp": m.timestamp, - "thread_id": m.thread_id, "format": m.format, "epoch": m.epoch, "device": m.device, -- cgit v1.2.3