# Proposal: P2P Message History Gossip ## Goals - New peers joining a network can retrieve recent message history from existing peers - No central storage — history lives only in peer daemons (SQLite) - No new trust requirements — history is shared only over already-established encrypted DataChannels - No duplicates in the local store or UI - No conflicts — history gossip is append-only and idempotent --- ## Privacy Model History is shared **peer-to-peer over the encrypted mesh**, never via the anchor. The anchor remains dumb — it sees only signaling blobs. A peer only receives history from peers they have successfully completed a YAW/2 handshake with, so the same trust boundary as live messages applies. A peer can choose not to share history by ignoring `history_request` messages — the protocol is advisory, not mandatory. --- ## Wire Protocol Two new YAW/2 extension messages (added to EXTENSIONS.md): ### `history_request` Sent by a newly-connected peer to one or more existing peers shortly after the handshake completes. ```json { "type": "history_request", "room": "general", "since": "2026-01-01T00:00:00Z", "limit": 200 } ``` | Field | Type | Description | |---------|-----------------|----------------------------------------------------------| | `room` | string | Room name to request history for. One request per room. | | `since` | ISO 8601 or "" | Only return messages newer than this timestamp. Empty = return up to `limit` most recent. | | `limit` | int (max 500) | Maximum number of messages to return. Responder may return fewer. | ### `history_chunk` Response from an existing peer. May be sent in multiple chunks if `limit` is large. ```json { "type": "history_chunk", "room": "general", "messages": [ { "id": "sha256:", "from": "", "from_id": "", "body": "hello", "ts": "2026-06-01T12:00:00Z", "room": "general" } ], "done": true } ``` | Field | Type | Description | |------------|---------|----------------------------------------------------------| | `messages` | array | Ordered oldest-first. | | `done` | bool | `true` on the final chunk. Receiver may display after this. | --- ## Message Identity and Deduplication Each message has a **content-addressed ID**: ``` id = "sha256:" + hex(SHA-256(from_id || room || ts || body)) ``` - Computed by the original sender and included in every live message going forward - The SQLite `messages` table gains an `id TEXT UNIQUE` column - On insert, use `INSERT OR IGNORE` — receiving the same message twice (live or via gossip) is a no-op - The UI sorts by `ts`, so late-arriving history slots in correctly without reordering visible messages Legacy messages (before this feature) have no `id`. They are assigned a local-only ID on migration and are never gossipped (they have no canonical ID the receiver could deduplicate against). --- ## Daemon-Side Implementation ### SQLite schema change ```sql ALTER TABLE messages ADD COLUMN msg_id TEXT; CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_msg_id ON messages(msg_id) WHERE msg_id IS NOT NULL; ``` ### Handling `history_request` ``` peer sends history_request{room, since, limit} → query SQLite: SELECT * FROM messages WHERE room=? AND ts>? ORDER BY ts ASC LIMIT ? → send history_chunk{room, messages, done:true} ``` Responder enforces: - `limit` capped at 500 - Only messages the responder itself received or sent (no re-gossipping of gossipped history to avoid amplification) - Rate limit: one `history_request` per peer per room per 60 seconds ### Handling `history_chunk` ``` for each message in chunk: INSERT OR IGNORE INTO messages (msg_id, room, from_alias, from_id, body, ts) VALUES (...) emit IPC event: history_loaded{room, count} ``` ### When to request - After handshake completes with the **first** peer in a network (only ask one peer — avoids fan-out) - Request rooms the local peer knows about (from its own SQLite `rooms` table) - If no rooms known yet: request `"general"` only; discover others from incoming live messages --- ## IPC / UI Integration New IPC event emitted after history is loaded: ```json { "type": "history_loaded", "network_id": "...", "room": "general", "count": 47 } ``` The web UI and TUI insert a visual separator above the first gossipped message: ``` ── 47 earlier messages ───────────────────────────── [12:03] alice: hey [12:04] bob: yo ── live ───────────────────────────────────────────── [14:22] you joined ``` --- ## What This Does Not Do - **No conflict resolution** — messages are immutable append-only records; there is nothing to conflict - **No ordering guarantee beyond timestamp** — if two peers sent messages at the same millisecond, both are stored; the UI sorts by `ts` then `msg_id` for a stable tiebreak - **No full sync** — gossip is bounded by `limit` and `since`; it is not a replication protocol - **No anchor involvement** — the anchor never sees history - **No history from peers who were offline** — if no peer with history is online when you join, you get nothing (acceptable given the trust model)