feat(mesh): MessageKey + Reply/Reaction variants and sender seq (Phase 2a)

Per-target outbound seq counter on MeshState allocates a monotonic seq
before each typed envelope is encoded; send_typed_wire +
send_channel_typed_wire record it (alongside our own pubkey_hex) on the
Sent MeshMessage so the local store carries the same MessageKey the
receiver will see. TypedEnvelope.with_seq lets the RPC layer stamp the
seq AFTER signing (signature covers t/v/ts only).

New MessageKey struct pairs sender_pubkey+sender_seq as the stable
cross-transport identity. Adds variants 13 Reply and 14 Reaction with
ReplyPayload {target, text} and ReactionPayload {target, emoji}, plus
mesh.send-reply / mesh.send-reaction RPCs and receive-side dispatch
arms that store the payload json for the UI to index.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-13 13:19:30 -04:00
co-authored by Claude Opus 4.6
parent a530a906b8
commit 4991c213ae
6 changed files with 235 additions and 33 deletions
+20 -5
View File
@@ -507,6 +507,11 @@ impl MeshService {
/// Send a typed envelope wire payload and record a rich Sent MeshMessage.
/// Used by RPC handlers that transmit invoice/coordinate/alert/etc. so the
/// UI sees a proper rich Sent card instead of garbage wire-byte plaintext.
///
/// `sender_seq` should match the seq encoded into the TypedEnvelope by the
/// caller — the RPC handler allocates it via `next_send_seq` before
/// building the envelope and threads it through here so the local Sent
/// record carries the same MessageKey the receiver will see.
pub async fn send_typed_wire(
&self,
contact_id: u32,
@@ -514,13 +519,21 @@ impl MeshService {
type_label: &str,
display_text: &str,
typed_payload: Option<serde_json::Value>,
sender_seq: u64,
) -> Result<MeshMessage> {
self.send_raw_payload(contact_id, wire).await?;
Ok(self
.record_sent_typed(contact_id, type_label, display_text, typed_payload)
.record_sent_typed(contact_id, type_label, display_text, typed_payload, sender_seq)
.await)
}
/// Allocate the next outbound seq for a target. Convenience passthrough
/// to MeshState::next_send_seq; used by RPC handlers before encoding a
/// TypedEnvelope so the seq on the wire matches the Sent record.
pub async fn next_send_seq(&self, target: u32) -> u64 {
self.state.next_send_seq(target).await
}
/// Broadcast a typed envelope wire payload on a mesh channel and record a
/// rich Sent MeshMessage. Bytes are sent directly — do NOT utf8_lossy-encode
/// binary envelope bytes before handing them here.
@@ -531,6 +544,7 @@ impl MeshService {
type_label: &str,
display_text: &str,
typed_payload: Option<serde_json::Value>,
sender_seq: u64,
) -> Result<MeshMessage> {
let status = self.state.status.read().await;
if !status.device_connected {
@@ -569,8 +583,8 @@ impl MeshService {
encrypted: false,
message_type: type_label.to_string(),
typed_payload,
sender_pubkey: None,
sender_seq: None,
sender_pubkey: Some(self.our_ed_pubkey_hex.clone()),
sender_seq: Some(sender_seq),
};
self.state.store_message(msg.clone()).await;
{
@@ -631,6 +645,7 @@ impl MeshService {
type_label: &str,
display_text: &str,
typed_payload: Option<serde_json::Value>,
sender_seq: u64,
) -> MeshMessage {
let msg_id = self.state.next_id().await;
let peer_name = self
@@ -651,8 +666,8 @@ impl MeshService {
encrypted: false,
message_type: type_label.to_string(),
typed_payload,
sender_pubkey: None,
sender_seq: None,
sender_pubkey: Some(self.our_ed_pubkey_hex.clone()),
sender_seq: Some(sender_seq),
};
self.state.store_message(msg.clone()).await;
{