=> ({
- [MessageState.Ack]: {
- displayText: t("deliveryStatus.delivered.displayText"),
- icon: CheckCircle2,
- ariaLabel: t("deliveryStatus.delivered.label"),
- iconClassName: "text-green-500",
- },
- [MessageState.Waiting]: {
- displayText: t("deliveryStatus.waiting.displayText"),
- icon: CircleEllipsis,
- ariaLabel: t("deliveryStatus.waiting.label"),
- iconClassName: "text-slate-400",
- },
- [MessageState.Failed]: {
- displayText: t("deliveryStatus.failed.displayText"),
- icon: AlertCircle,
- ariaLabel: t("deliveryStatus.failed.label"),
- iconClassName: "text-red-500 dark:text-red-400",
- },
- }),
- [t],
- );
-
- const UNKNOWN_STATUS = useMemo(
- (): MessageStatusInfo => ({
- displayText: t("deliveryStatus.unknown.displayText"),
- icon: AlertCircle,
- ariaLabel: t("deliveryStatus.unknown.label"),
- iconClassName: "text-red-500 dark:text-red-400",
- }),
- [t],
- );
-
- const getMessageStatusInfo = useMemo(
- () =>
- (state: MessageState): MessageStatusInfo =>
- MESSAGE_STATUS_MAP[state] ?? UNKNOWN_STATUS,
- [MESSAGE_STATUS_MAP, UNKNOWN_STATUS],
- );
-
const messageUser: Protobuf.Mesh.NodeInfo | null | undefined =
message.from != null ? (messageUserNode ?? null) : null;
@@ -164,7 +121,10 @@ export const MessageItem = ({ message }: MessageItemProps) => {
};
}, [messageUser, message.from, t, myNodeNum]);
- const messageStatusInfo = getMessageStatusInfo(message.state);
+ const messageStatusInfo = useMemo(
+ () => getMessageDeliveryStatusInfo(message, t),
+ [message, t],
+ );
const StatusIconComponent = messageStatusInfo.icon;
const messageDate = useMemo(
@@ -193,8 +153,8 @@ export const MessageItem = ({ message }: MessageItemProps) => {
);
const isSender = myNodeNum !== undefined && message.from === myNodeNum;
- const isOnPrimaryChannel = message.channel === Types.ChannelNumber.Primary; // Use the enum
- const shouldShowStatusIcon = isSender && isOnPrimaryChannel;
+ const shouldShowStatus = isSender;
+ const canRetry = shouldShowStatus && messageStatusInfo.canRetry && onRetry;
const messageItemWrapperClass = cn(
"group w-full py-2 relative list-none",
@@ -228,24 +188,49 @@ export const MessageItem = ({ message }: MessageItemProps) => {
{fullDateTime}
)}
- {shouldShowStatusIcon && (
+
+
+ {message?.message && (
+
+ {message.message}
+
+ )}
+
+ {shouldShowStatus && (
+
-
+
+ {messageStatusInfo.displayText}
- )}
-
-
- {message?.message && (
-
- {message.message}
+ {canRetry && (
+
+ )}
)}
diff --git a/apps/web/src/components/PageComponents/Messages/messageDeliveryStatus.test.ts b/apps/web/src/components/PageComponents/Messages/messageDeliveryStatus.test.ts
new file mode 100644
index 000000000..e66de767e
--- /dev/null
+++ b/apps/web/src/components/PageComponents/Messages/messageDeliveryStatus.test.ts
@@ -0,0 +1,129 @@
+import { MessageState, MessageType } from "@core/stores/messageStore";
+import { Protobuf } from "@meshtastic/sdk";
+import { describe, expect, it } from "vitest";
+import { getMessageDeliveryStatusInfo } from "./messageDeliveryStatus.ts";
+
+const strings: Record = {
+ "deliveryStatus.deliveredToMesh.displayText": "Delivered to mesh",
+ "deliveryStatus.deliveredToMesh.detailText":
+ "This channel message was acknowledged by the mesh.",
+ "deliveryStatus.deliveredToRecipient.displayText": "Delivered to recipient",
+ "deliveryStatus.deliveredToRecipient.detailText":
+ "The recipient confirmed this message.",
+ "deliveryStatus.encryptedSendFailed.displayText":
+ "Could not send encrypted message",
+ "deliveryStatus.encryptedSendFailed.detailText":
+ "Key exchange or contact information may be out of date. Wait for node info to sync, then try again.",
+ "deliveryStatus.failedToMesh.displayText": "Failed to deliver to mesh",
+ "deliveryStatus.failedToMesh.detailText":
+ "No node confirmed this message. Try again when you have better signal or more mesh coverage.",
+ "deliveryStatus.messageTooLarge.displayText": "Message is too large to send",
+ "deliveryStatus.messageTooLarge.detailText":
+ "Shorten the message and send it again.",
+ "deliveryStatus.noChannel.displayText": "No channel selected",
+ "deliveryStatus.noChannel.detailText": "Select a channel before sending.",
+ "deliveryStatus.recipientKeyUnavailable.displayText":
+ "Recipient key unavailable",
+ "deliveryStatus.recipientKeyUnavailable.detailText":
+ "Your node does not have the recipient's public key yet. Wait for node info to sync, then try again.",
+ "deliveryStatus.recipientNeedsYourKey.displayText":
+ "Recipient needs your key",
+ "deliveryStatus.recipientNeedsYourKey.detailText":
+ "The recipient does not know your public key yet. Your node may share its info automatically; try again after it syncs.",
+ "deliveryStatus.relayed.displayText": "Relayed, not confirmed by recipient",
+ "deliveryStatus.relayed.detailText":
+ "A node relayed this message, but the recipient has not confirmed it.",
+ "deliveryStatus.sending.displayText": "Sending...",
+ "deliveryStatus.sending.detailText": "This message is being sent.",
+ "deliveryStatus.unknown.displayText": "Message status unknown",
+ "deliveryStatus.unknown.detailText": "The message status is not available.",
+};
+
+const t = ((key: string) => strings[key] ?? key) as never;
+
+describe("getMessageDeliveryStatusInfo", () => {
+ it.each([
+ [
+ MessageState.Waiting,
+ MessageType.Broadcast,
+ undefined,
+ "Sending...",
+ false,
+ ],
+ [
+ MessageState.Ack,
+ MessageType.Broadcast,
+ undefined,
+ "Delivered to mesh",
+ false,
+ ],
+ [
+ MessageState.Ack,
+ MessageType.Direct,
+ undefined,
+ "Delivered to recipient",
+ false,
+ ],
+ [
+ MessageState.Relayed,
+ MessageType.Direct,
+ undefined,
+ "Relayed, not confirmed by recipient",
+ true,
+ ],
+ [
+ MessageState.Failed,
+ MessageType.Broadcast,
+ Protobuf.Mesh.Routing_Error.MAX_RETRANSMIT,
+ "Failed to deliver to mesh",
+ true,
+ ],
+ [
+ MessageState.Failed,
+ MessageType.Broadcast,
+ Protobuf.Mesh.Routing_Error.NO_CHANNEL,
+ "No channel selected",
+ true,
+ ],
+ [
+ MessageState.Failed,
+ MessageType.Direct,
+ Protobuf.Mesh.Routing_Error.PKI_FAILED,
+ "Could not send encrypted message",
+ true,
+ ],
+ [
+ MessageState.Failed,
+ MessageType.Direct,
+ Protobuf.Mesh.Routing_Error.PKI_SEND_FAIL_PUBLIC_KEY,
+ "Recipient key unavailable",
+ true,
+ ],
+ [
+ MessageState.Failed,
+ MessageType.Direct,
+ Protobuf.Mesh.Routing_Error.PKI_UNKNOWN_PUBKEY,
+ "Recipient needs your key",
+ true,
+ ],
+ [
+ MessageState.Failed,
+ MessageType.Broadcast,
+ Protobuf.Mesh.Routing_Error.TOO_LARGE,
+ "Message is too large to send",
+ false,
+ ],
+ ])(
+ "maps %s/%s/%s to %s",
+ (state, type, routingError, displayText, canRetry) => {
+ const info = getMessageDeliveryStatusInfo(
+ { state, type, routingError },
+ t,
+ );
+
+ expect(info.displayText).toBe(displayText);
+ expect(info.ariaLabel).toBe(displayText);
+ expect(info.canRetry).toBe(canRetry);
+ },
+ );
+});
diff --git a/apps/web/src/components/PageComponents/Messages/messageDeliveryStatus.ts b/apps/web/src/components/PageComponents/Messages/messageDeliveryStatus.ts
new file mode 100644
index 000000000..c2b20fdf1
--- /dev/null
+++ b/apps/web/src/components/PageComponents/Messages/messageDeliveryStatus.ts
@@ -0,0 +1,129 @@
+import { MessageState, MessageType } from "@core/stores/messageStore";
+import { Protobuf } from "@meshtastic/sdk";
+import type { TFunction } from "i18next";
+import type { LucideIcon } from "lucide-react";
+import {
+ AlertCircle,
+ CheckCircle2,
+ CircleEllipsis,
+ RefreshCcw,
+} from "lucide-react";
+
+export interface MessageDeliveryInput {
+ state: MessageState;
+ type: MessageType;
+ routingError?: Protobuf.Mesh.Routing_Error;
+}
+
+export interface MessageDeliveryStatusInfo {
+ displayText: string;
+ detailText: string;
+ icon: LucideIcon;
+ ariaLabel: string;
+ iconClassName: string;
+ textClassName: string;
+ canRetry: boolean;
+}
+
+export function getMessageDeliveryStatusInfo(
+ message: MessageDeliveryInput,
+ t: TFunction<"messages", undefined>,
+): MessageDeliveryStatusInfo {
+ switch (message.state) {
+ case MessageState.Waiting:
+ return {
+ displayText: t("deliveryStatus.sending.displayText"),
+ detailText: t("deliveryStatus.sending.detailText"),
+ icon: CircleEllipsis,
+ ariaLabel: t("deliveryStatus.sending.displayText"),
+ iconClassName: "text-amber-500 dark:text-amber-400",
+ textClassName: "text-amber-700 dark:text-amber-300",
+ canRetry: false,
+ };
+ case MessageState.Ack:
+ return message.type === MessageType.Direct
+ ? {
+ displayText: t("deliveryStatus.deliveredToRecipient.displayText"),
+ detailText: t("deliveryStatus.deliveredToRecipient.detailText"),
+ icon: CheckCircle2,
+ ariaLabel: t("deliveryStatus.deliveredToRecipient.displayText"),
+ iconClassName: "text-green-600 dark:text-green-400",
+ textClassName: "text-green-700 dark:text-green-300",
+ canRetry: false,
+ }
+ : {
+ displayText: t("deliveryStatus.deliveredToMesh.displayText"),
+ detailText: t("deliveryStatus.deliveredToMesh.detailText"),
+ icon: CheckCircle2,
+ ariaLabel: t("deliveryStatus.deliveredToMesh.displayText"),
+ iconClassName: "text-slate-500 dark:text-slate-400",
+ textClassName: "text-slate-500 dark:text-slate-400",
+ canRetry: false,
+ };
+ case MessageState.Relayed:
+ return {
+ displayText: t("deliveryStatus.relayed.displayText"),
+ detailText: t("deliveryStatus.relayed.detailText"),
+ icon: RefreshCcw,
+ ariaLabel: t("deliveryStatus.relayed.displayText"),
+ iconClassName: "text-amber-500 dark:text-amber-400",
+ textClassName: "text-amber-700 dark:text-amber-300",
+ canRetry: true,
+ };
+ case MessageState.Failed:
+ return getFailureStatusInfo(message.routingError, t);
+ default:
+ return {
+ displayText: t("deliveryStatus.unknown.displayText"),
+ detailText: t("deliveryStatus.unknown.detailText"),
+ icon: AlertCircle,
+ ariaLabel: t("deliveryStatus.unknown.displayText"),
+ iconClassName: "text-red-500 dark:text-red-400",
+ textClassName: "text-red-700 dark:text-red-300",
+ canRetry: true,
+ };
+ }
+}
+
+function getFailureStatusInfo(
+ routingError: Protobuf.Mesh.Routing_Error | undefined,
+ t: TFunction<"messages", undefined>,
+): MessageDeliveryStatusInfo {
+ const isPermanent = routingError === Protobuf.Mesh.Routing_Error.TOO_LARGE;
+ const failureKey = failureTranslationKey(routingError);
+
+ return {
+ displayText: t(`deliveryStatus.${failureKey}.displayText`),
+ detailText: t(`deliveryStatus.${failureKey}.detailText`),
+ icon: AlertCircle,
+ ariaLabel: t(`deliveryStatus.${failureKey}.displayText`),
+ iconClassName: isPermanent
+ ? "text-red-500 dark:text-red-400"
+ : "text-amber-500 dark:text-amber-400",
+ textClassName: isPermanent
+ ? "text-red-700 dark:text-red-300"
+ : "text-amber-700 dark:text-amber-300",
+ canRetry: !isPermanent,
+ };
+}
+
+function failureTranslationKey(
+ routingError: Protobuf.Mesh.Routing_Error | undefined,
+): string {
+ switch (routingError) {
+ case Protobuf.Mesh.Routing_Error.NO_CHANNEL:
+ return "noChannel";
+ case Protobuf.Mesh.Routing_Error.PKI_FAILED:
+ return "encryptedSendFailed";
+ case Protobuf.Mesh.Routing_Error.PKI_SEND_FAIL_PUBLIC_KEY:
+ return "recipientKeyUnavailable";
+ case Protobuf.Mesh.Routing_Error.PKI_UNKNOWN_PUBKEY:
+ return "recipientNeedsYourKey";
+ case Protobuf.Mesh.Routing_Error.TOO_LARGE:
+ return "messageTooLarge";
+ case Protobuf.Mesh.Routing_Error.MAX_RETRANSMIT:
+ case Protobuf.Mesh.Routing_Error.TIMEOUT:
+ default:
+ return "failedToMesh";
+ }
+}
diff --git a/apps/web/src/core/hooks/useChatAsLegacyMessages.ts b/apps/web/src/core/hooks/useChatAsLegacyMessages.ts
index f8bb64c89..9241527cd 100644
--- a/apps/web/src/core/hooks/useChatAsLegacyMessages.ts
+++ b/apps/web/src/core/hooks/useChatAsLegacyMessages.ts
@@ -64,6 +64,7 @@ function toLegacy(
date: message.rxTime.getTime(),
messageId: message.id,
state: mapState(message.state),
+ routingError: message.routingError,
message: message.text,
} as LegacyMessage;
}
@@ -72,6 +73,8 @@ function mapState(state: SdkMessageState): LegacyMessageState {
switch (state) {
case SdkMessageState.Ack:
return LegacyMessageState.Ack;
+ case SdkMessageState.Relayed:
+ return LegacyMessageState.Relayed;
case SdkMessageState.Failed:
return LegacyMessageState.Failed;
case SdkMessageState.Pending:
diff --git a/apps/web/src/core/hooks/useChatLegacy.ts b/apps/web/src/core/hooks/useChatLegacy.ts
index f8bb64c89..9241527cd 100644
--- a/apps/web/src/core/hooks/useChatLegacy.ts
+++ b/apps/web/src/core/hooks/useChatLegacy.ts
@@ -64,6 +64,7 @@ function toLegacy(
date: message.rxTime.getTime(),
messageId: message.id,
state: mapState(message.state),
+ routingError: message.routingError,
message: message.text,
} as LegacyMessage;
}
@@ -72,6 +73,8 @@ function mapState(state: SdkMessageState): LegacyMessageState {
switch (state) {
case SdkMessageState.Ack:
return LegacyMessageState.Ack;
+ case SdkMessageState.Relayed:
+ return LegacyMessageState.Relayed;
case SdkMessageState.Failed:
return LegacyMessageState.Failed;
case SdkMessageState.Pending:
diff --git a/apps/web/src/core/stores/messageStore/index.ts b/apps/web/src/core/stores/messageStore/index.ts
index 89df8125c..56265df02 100644
--- a/apps/web/src/core/stores/messageStore/index.ts
+++ b/apps/web/src/core/stores/messageStore/index.ts
@@ -5,10 +5,11 @@
* persistence is owned by the SDK ChatClient + SqlocalMessageRepository.
*/
-import type { Types } from "@meshtastic/sdk";
+import type { Protobuf, Types } from "@meshtastic/sdk";
export enum MessageState {
Ack = "ack",
+ Relayed = "relayed",
Waiting = "waiting",
Failed = "failed",
}
@@ -30,6 +31,7 @@ interface MessageBase {
date: number;
messageId: number;
state: MessageState;
+ routingError?: Protobuf.Mesh.Routing_Error;
message: string;
}
diff --git a/apps/web/src/pages/Messages.tsx b/apps/web/src/pages/Messages.tsx
index 67caf3afd..b5482fffa 100644
--- a/apps/web/src/pages/Messages.tsx
+++ b/apps/web/src/pages/Messages.tsx
@@ -11,6 +11,7 @@ import { useChatAsLegacyMessages } from "@core/hooks/useChatAsLegacyMessages.ts"
import { useNodesAsProto } from "@core/hooks/useNodesAsProto.ts";
import { useToast } from "@core/hooks/useToast.ts";
import { MessageType, useSidebar } from "@core/stores";
+import type { Message } from "@core/stores/messageStore";
import { cn } from "@core/utils/cn.ts";
import { Protobuf, Types } from "@meshtastic/sdk";
import {
@@ -166,6 +167,26 @@ export const MessagesPage = () => {
[meshClient, numericChatId, isDirect],
);
+ const retryMessage = useCallback(
+ async (message: Message) => {
+ if (!meshClient) {
+ console.warn("[MessagesPage] no active mesh client; retry dropped");
+ return;
+ }
+ const destination: Types.Destination =
+ message.type === MessageType.Direct ? message.to : "broadcast";
+ const result = await meshClient.chat.send({
+ text: message.message,
+ destination,
+ channel: message.channel,
+ });
+ if (result.status === "error") {
+ console.error("Failed to retry message:", result.error);
+ }
+ },
+ [meshClient],
+ );
+
const broadcastMessages = useChatAsLegacyMessages({
type: MessageType.Broadcast,
channelId: numericChatId,
@@ -178,9 +199,19 @@ export const MessagesPage = () => {
const renderChatContent = () => {
switch (chatType) {
case MessageType.Broadcast:
- return ;
+ return (
+
+ );
case MessageType.Direct:
- return ;
+ return (
+
+ );
default:
return ;
}
diff --git a/packages/sdk-storage-sqlocal/src/chat/SqlocalMessageRepository.test.ts b/packages/sdk-storage-sqlocal/src/chat/SqlocalMessageRepository.test.ts
index 10335d1a6..fa7060cd0 100644
--- a/packages/sdk-storage-sqlocal/src/chat/SqlocalMessageRepository.test.ts
+++ b/packages/sdk-storage-sqlocal/src/chat/SqlocalMessageRepository.test.ts
@@ -1,4 +1,9 @@
-import { ChannelNumber, type Message, MessageState } from "@meshtastic/sdk";
+import {
+ ChannelNumber,
+ type Message,
+ MessageState,
+ Protobuf,
+} from "@meshtastic/sdk";
import { beforeEach, describe, expect, it } from "vitest";
import type { SqlocalDb } from "../db.ts";
import { createMemoryDb } from "../testing/createMemoryDb.ts";
@@ -52,12 +57,17 @@ describe("SqlocalMessageRepository (sql.js test driver)", () => {
it("updateState mutates the matching row", async () => {
await repo.append(msg(42, 1000));
- await repo.updateState(42, MessageState.Failed);
+ await repo.updateState(
+ 42,
+ MessageState.Failed,
+ Protobuf.Mesh.Routing_Error.NO_CHANNEL,
+ );
const [found] = await repo.loadRecent(
{ kind: "channel", channel: ChannelNumber.Primary },
1,
);
expect(found?.state).toBe(MessageState.Failed);
+ expect(found?.routingError).toBe(Protobuf.Mesh.Routing_Error.NO_CHANNEL);
});
it("prune enforces maxPerBucket", async () => {
diff --git a/packages/sdk-storage-sqlocal/src/chat/SqlocalMessageRepository.ts b/packages/sdk-storage-sqlocal/src/chat/SqlocalMessageRepository.ts
index adabf056d..e15d7814f 100644
--- a/packages/sdk-storage-sqlocal/src/chat/SqlocalMessageRepository.ts
+++ b/packages/sdk-storage-sqlocal/src/chat/SqlocalMessageRepository.ts
@@ -1,3 +1,4 @@
+import type * as Protobuf from "@meshtastic/protobufs";
import type {
ConversationKey,
Message,
@@ -63,10 +64,14 @@ export class SqlocalMessageRepository implements MessageRepository {
this.notify(input);
}
- async updateState(id: number, state: MessageState): Promise {
+ async updateState(
+ id: number,
+ state: MessageState,
+ routingError?: Protobuf.Mesh.Routing_Error,
+ ): Promise {
await this.db
.update(messages)
- .set({ state })
+ .set({ state, routingError: routingError ?? null })
.where(and(eq(messages.deviceId, this.deviceId), eq(messages.id, id))!);
}
@@ -149,7 +154,8 @@ interface MessageRow {
rxTime: number;
type: "broadcast" | "direct";
text: string;
- state: "pending" | "ack" | "failed";
+ state: "pending" | "ack" | "relayed" | "failed";
+ routingError: number | null;
}
function rowToMessage(row: MessageRow): Message {
@@ -162,6 +168,7 @@ function rowToMessage(row: MessageRow): Message {
type: row.type,
text: row.text,
state: row.state as MessageState,
+ routingError: row.routingError ?? undefined,
};
}
@@ -181,5 +188,6 @@ function messageToRow(deviceId: number, message: Message): MessageRow {
type: message.type,
text: message.text,
state: message.state,
+ routingError: message.routingError ?? null,
};
}
diff --git a/packages/sdk-storage-sqlocal/src/schema/chat.ts b/packages/sdk-storage-sqlocal/src/schema/chat.ts
index 2783d7e05..e278f3e4e 100644
--- a/packages/sdk-storage-sqlocal/src/schema/chat.ts
+++ b/packages/sdk-storage-sqlocal/src/schema/chat.ts
@@ -18,7 +18,10 @@ export const messages = sqliteTable(
rxTime: integer("rx_time").notNull(),
type: text("type", { enum: ["broadcast", "direct"] }).notNull(),
text: text("text").notNull(),
- state: text("state", { enum: ["pending", "ack", "failed"] }).notNull(),
+ state: text("state", {
+ enum: ["pending", "ack", "relayed", "failed"],
+ }).notNull(),
+ routingError: integer("routing_error"),
},
(t) => ({
pk: index("messages_pk").on(t.deviceId, t.id),
diff --git a/packages/sdk-storage-sqlocal/src/schema/migrations.ts b/packages/sdk-storage-sqlocal/src/schema/migrations.ts
index 7bb6343ea..129620d1f 100644
--- a/packages/sdk-storage-sqlocal/src/schema/migrations.ts
+++ b/packages/sdk-storage-sqlocal/src/schema/migrations.ts
@@ -65,4 +65,8 @@ export const MIGRATIONS: ReadonlyArray<{ version: number; sql: string[] }> = [
)`,
],
},
+ {
+ version: 3,
+ sql: [`ALTER TABLE messages ADD COLUMN routing_error INTEGER`],
+ },
];
diff --git a/packages/sdk/src/core/packet-codec/decodePacket.ts b/packages/sdk/src/core/packet-codec/decodePacket.ts
index 363e3f4c2..242246b51 100644
--- a/packages/sdk/src/core/packet-codec/decodePacket.ts
+++ b/packages/sdk/src/core/packet-codec/decodePacket.ts
@@ -295,6 +295,7 @@ function handleDecodedPacket(
) {
const packetMetadata: Omit, "data"> = {
id: meshPacket.id,
+ requestId: dataPacket.requestId,
rxTime: new Date(meshPacket.rxTime * 1000),
type: meshPacket.to === Constants.broadcastNum ? "broadcast" : "direct",
from: meshPacket.from,
diff --git a/packages/sdk/src/core/types.ts b/packages/sdk/src/core/types.ts
index 6b2a1f936..7e21c2572 100644
--- a/packages/sdk/src/core/types.ts
+++ b/packages/sdk/src/core/types.ts
@@ -21,6 +21,7 @@ export type PacketDestination = "broadcast" | "direct";
export interface PacketMetadata {
id: number;
+ requestId?: number;
rxTime: Date;
type: PacketDestination;
from: number;
diff --git a/packages/sdk/src/features/chat/ChatClient.send.test.ts b/packages/sdk/src/features/chat/ChatClient.send.test.ts
index dd6564de2..2af75d095 100644
--- a/packages/sdk/src/features/chat/ChatClient.send.test.ts
+++ b/packages/sdk/src/features/chat/ChatClient.send.test.ts
@@ -1,4 +1,5 @@
import type { ResultType } from "better-result";
+import * as Protobuf from "@meshtastic/protobufs";
import { describe, expect, it } from "vitest";
import { MeshClient } from "../../core/client/MeshClient.ts";
import { createFakeTransport } from "../../core/testing/createFakeTransport.ts";
@@ -140,7 +141,7 @@ describe("ChatClient.send optimistic append", () => {
expect(messages.value[0]!.state).toBe(MessageState.Failed);
});
- it("flips outbound state to Ack on a routing-error=NONE packet matching the id", async () => {
+ it("flips outbound channel state to Ack on a routing-error=NONE packet matching the request id", async () => {
const { transport } = createFakeTransport();
const client = new MeshClient({ transport });
const messages = client.chat.messages(ChannelNumber.Primary);
@@ -149,15 +150,111 @@ describe("ChatClient.send optimistic append", () => {
if (result.status !== "ok") throw new Error("send failed");
client.events.onRoutingPacket.dispatch({
- id: result.value,
+ id: 987,
+ requestId: result.value,
from: 0,
to: 0,
channel: 0,
type: "broadcast",
rxTime: new Date(),
- data: { variant: { case: "errorReason", value: 0 } },
+ data: {
+ variant: {
+ case: "errorReason",
+ value: Protobuf.Mesh.Routing_Error.NONE,
+ },
+ },
} as never);
expect(messages.value[0]!.state).toBe(MessageState.Ack);
});
+
+ it("marks direct NONE from a relayer as relayed instead of recipient-delivered", async () => {
+ const { transport } = createFakeTransport();
+ const client = new MeshClient({ transport });
+ const peer = 12345;
+ const direct = client.chat.direct(peer);
+
+ const result: SendResult = await withAckFlush(client, () =>
+ client.chat.send({ text: "hi peer", destination: peer }),
+ );
+ if (result.status !== "ok") throw new Error("send failed");
+
+ client.events.onRoutingPacket.dispatch({
+ id: 456,
+ requestId: result.value,
+ from: 999,
+ to: client.myNodeNum,
+ channel: 0,
+ type: "direct",
+ rxTime: new Date(),
+ data: {
+ variant: {
+ case: "errorReason",
+ value: Protobuf.Mesh.Routing_Error.NONE,
+ },
+ },
+ } as never);
+
+ expect(direct.value[0]!.state).toBe(MessageState.Relayed);
+ });
+
+ it("marks direct NONE from the recipient as recipient-delivered", async () => {
+ const { transport } = createFakeTransport();
+ const client = new MeshClient({ transport });
+ const peer = 12345;
+ const direct = client.chat.direct(peer);
+
+ const result: SendResult = await withAckFlush(client, () =>
+ client.chat.send({ text: "hi peer", destination: peer }),
+ );
+ if (result.status !== "ok") throw new Error("send failed");
+
+ client.events.onRoutingPacket.dispatch({
+ id: 789,
+ requestId: result.value,
+ from: peer,
+ to: client.myNodeNum,
+ channel: 0,
+ type: "direct",
+ rxTime: new Date(),
+ data: {
+ variant: {
+ case: "errorReason",
+ value: Protobuf.Mesh.Routing_Error.NONE,
+ },
+ },
+ } as never);
+
+ expect(direct.value[0]!.state).toBe(MessageState.Ack);
+ });
+
+ it("preserves routing error reason on failed delivery", async () => {
+ const { transport } = createFakeTransport();
+ const client = new MeshClient({ transport });
+ const messages = client.chat.messages(ChannelNumber.Primary);
+
+ const result = await sendBroadcast(client, "no ack");
+ if (result.status !== "ok") throw new Error("send failed");
+
+ client.events.onRoutingPacket.dispatch({
+ id: 321,
+ requestId: result.value,
+ from: 0,
+ to: 0,
+ channel: 0,
+ type: "broadcast",
+ rxTime: new Date(),
+ data: {
+ variant: {
+ case: "errorReason",
+ value: Protobuf.Mesh.Routing_Error.MAX_RETRANSMIT,
+ },
+ },
+ } as never);
+
+ expect(messages.value[0]!.state).toBe(MessageState.Failed);
+ expect(messages.value[0]!.routingError).toBe(
+ Protobuf.Mesh.Routing_Error.MAX_RETRANSMIT,
+ );
+ });
});
diff --git a/packages/sdk/src/features/chat/ChatClient.ts b/packages/sdk/src/features/chat/ChatClient.ts
index 5116cc2ad..e3993a69f 100644
--- a/packages/sdk/src/features/chat/ChatClient.ts
+++ b/packages/sdk/src/features/chat/ChatClient.ts
@@ -1,3 +1,4 @@
+import * as Protobuf from "@meshtastic/protobufs";
import { signal } from "@preact/signals-core";
import type { ResultType } from "better-result";
import type { MeshClient } from "../../core/client/MeshClient.ts";
@@ -22,6 +23,7 @@ import { InMemoryMessageRepository } from "./infrastructure/repositories/InMemor
import {
type SendTextError,
type SendTextInput,
+ MessageTooLongError,
sendText,
} from "./application/SendTextUseCase.ts";
import { ChatStore } from "./state/chatStore.ts";
@@ -132,12 +134,21 @@ export class ChatClient {
client.events.onRoutingPacket.subscribe((packet) => {
if (packet.data.variant.case === "errorReason") {
+ const messageId = packet.requestId ?? packet.id;
+ const reason = packet.data.variant.value;
+ const existing = this.store.findMessage(messageId);
const state =
- packet.data.variant.value === 0
- ? MessageState.Ack
+ reason === Protobuf.Mesh.Routing_Error.NONE
+ ? existing?.type === "direct" && packet.from !== existing.to
+ ? MessageState.Relayed
+ : MessageState.Ack
: MessageState.Failed;
- this.store.updateState(packet.id, state);
- void this.repository.updateState(packet.id, state).catch(() => {});
+ const routingError =
+ reason === Protobuf.Mesh.Routing_Error.NONE ? undefined : reason;
+ this.store.updateState(messageId, state, routingError);
+ void this.repository
+ .updateState(messageId, state, routingError)
+ .catch(() => {});
}
});
}
@@ -239,9 +250,13 @@ export class ChatClient {
// Send pipeline failed (transport closed, validation, etc.). Keep
// the optimistic message visible but mark it Failed so the user
// sees the error state next to their bubble.
- this.store.updateState(packetId, MessageState.Failed);
+ const routingError =
+ result.error instanceof MessageTooLongError
+ ? Protobuf.Mesh.Routing_Error.TOO_LARGE
+ : undefined;
+ this.store.updateState(packetId, MessageState.Failed, routingError);
void this.repository
- .updateState(packetId, MessageState.Failed)
+ .updateState(packetId, MessageState.Failed, routingError)
.catch(() => {});
}
return result;
diff --git a/packages/sdk/src/features/chat/domain/Message.ts b/packages/sdk/src/features/chat/domain/Message.ts
index 356b47563..c050688f8 100644
--- a/packages/sdk/src/features/chat/domain/Message.ts
+++ b/packages/sdk/src/features/chat/domain/Message.ts
@@ -1,3 +1,4 @@
+import type * as Protobuf from "@meshtastic/protobufs";
import type { ChannelNumber, PacketDestination } from "../../../core/types.ts";
import type { MessageState } from "./MessageState.ts";
@@ -13,4 +14,5 @@ export interface Message {
readonly type: PacketDestination;
readonly text: string;
readonly state: MessageState;
+ readonly routingError?: Protobuf.Mesh.Routing_Error;
}
diff --git a/packages/sdk/src/features/chat/domain/MessageRepository.ts b/packages/sdk/src/features/chat/domain/MessageRepository.ts
index 4f86fe807..08ef90f61 100644
--- a/packages/sdk/src/features/chat/domain/MessageRepository.ts
+++ b/packages/sdk/src/features/chat/domain/MessageRepository.ts
@@ -1,3 +1,4 @@
+import type * as Protobuf from "@meshtastic/protobufs";
import type { ChannelNumber } from "../../../core/types.ts";
import type { Message } from "./Message.ts";
import type { MessageState } from "./MessageState.ts";
@@ -33,7 +34,11 @@ export interface MessageRepository {
): Promise;
append(message: Message): Promise;
appendBatch(messages: ReadonlyArray): Promise;
- updateState(id: number, state: MessageState): Promise;
+ updateState(
+ id: number,
+ state: MessageState,
+ routingError?: Protobuf.Mesh.Routing_Error,
+ ): Promise;
prune(policy: RetentionPolicy): Promise;
/** Deletes every message in a single conversation. */
clearConversation(key: ConversationKey): Promise;
diff --git a/packages/sdk/src/features/chat/domain/MessageState.ts b/packages/sdk/src/features/chat/domain/MessageState.ts
index 54090fbc8..505096fb8 100644
--- a/packages/sdk/src/features/chat/domain/MessageState.ts
+++ b/packages/sdk/src/features/chat/domain/MessageState.ts
@@ -1,5 +1,6 @@
export enum MessageState {
Pending = "pending",
Ack = "ack",
+ Relayed = "relayed",
Failed = "failed",
}
diff --git a/packages/sdk/src/features/chat/infrastructure/MessageMapper.ts b/packages/sdk/src/features/chat/infrastructure/MessageMapper.ts
index 63a97eef8..54af3d678 100644
--- a/packages/sdk/src/features/chat/infrastructure/MessageMapper.ts
+++ b/packages/sdk/src/features/chat/infrastructure/MessageMapper.ts
@@ -16,6 +16,7 @@ export const MessageMapper = {
type: packet.type,
text: packet.data,
state,
+ routingError: undefined,
};
},
};
diff --git a/packages/sdk/src/features/chat/infrastructure/repositories/InMemoryMessageRepository.test.ts b/packages/sdk/src/features/chat/infrastructure/repositories/InMemoryMessageRepository.test.ts
index 58760587b..d8fdb7bb3 100644
--- a/packages/sdk/src/features/chat/infrastructure/repositories/InMemoryMessageRepository.test.ts
+++ b/packages/sdk/src/features/chat/infrastructure/repositories/InMemoryMessageRepository.test.ts
@@ -1,3 +1,4 @@
+import * as Protobuf from "@meshtastic/protobufs";
import { describe, expect, it } from "vitest";
import { ChannelNumber } from "../../../../core/types.ts";
import type { Message } from "../../domain/Message.ts";
@@ -47,12 +48,19 @@ describe("InMemoryMessageRepository", () => {
it("updateState mutates the matching message", async () => {
const repo = new InMemoryMessageRepository();
await repo.append(msg(42, 1000));
- await repo.updateState(42, MessageState.Failed);
+ await repo.updateState(
+ 42,
+ MessageState.Failed,
+ Protobuf.Mesh.Routing_Error.MAX_RETRANSMIT,
+ );
const [found] = await repo.loadRecent(
{ kind: "channel", channel: ChannelNumber.Primary },
1,
);
expect(found?.state).toBe(MessageState.Failed);
+ expect(found?.routingError).toBe(
+ Protobuf.Mesh.Routing_Error.MAX_RETRANSMIT,
+ );
});
it("prune enforces maxPerBucket", async () => {
diff --git a/packages/sdk/src/features/chat/infrastructure/repositories/InMemoryMessageRepository.ts b/packages/sdk/src/features/chat/infrastructure/repositories/InMemoryMessageRepository.ts
index a3f6801ef..2591610a9 100644
--- a/packages/sdk/src/features/chat/infrastructure/repositories/InMemoryMessageRepository.ts
+++ b/packages/sdk/src/features/chat/infrastructure/repositories/InMemoryMessageRepository.ts
@@ -1,3 +1,4 @@
+import type * as Protobuf from "@meshtastic/protobufs";
import type { Message } from "../../domain/Message.ts";
import type { MessageState } from "../../domain/MessageState.ts";
import {
@@ -45,13 +46,17 @@ export class InMemoryMessageRepository implements MessageRepository {
}
}
- async updateState(id: number, state: MessageState): Promise {
+ async updateState(
+ id: number,
+ state: MessageState,
+ routingError?: Protobuf.Mesh.Routing_Error,
+ ): Promise {
for (const bucket of this.buckets.values()) {
const idx = bucket.findIndex((m) => m.id === id);
if (idx !== -1) {
const existing = bucket[idx];
if (!existing) continue;
- bucket[idx] = { ...existing, state };
+ bucket[idx] = { ...existing, state, routingError };
return;
}
}
diff --git a/packages/sdk/src/features/chat/state/chatStore.ts b/packages/sdk/src/features/chat/state/chatStore.ts
index ea0926076..5c127f684 100644
--- a/packages/sdk/src/features/chat/state/chatStore.ts
+++ b/packages/sdk/src/features/chat/state/chatStore.ts
@@ -1,3 +1,4 @@
+import type * as Protobuf from "@meshtastic/protobufs";
import { type Signal, signal } from "@preact/signals-core";
import type { ReadonlySignal } from "../../../core/signals/createStore.ts";
import { toReadonly } from "../../../core/signals/createStore.ts";
@@ -73,14 +74,26 @@ export class ChatStore {
}
}
- updateState(id: number, state: MessageState): void {
+ findMessage(id: number): Message | undefined {
+ for (const bucket of this.buckets.values()) {
+ const found = bucket.value.find((m) => m.id === id);
+ if (found) return found;
+ }
+ return undefined;
+ }
+
+ updateState(
+ id: number,
+ state: MessageState,
+ routingError?: Protobuf.Mesh.Routing_Error,
+ ): void {
for (const [, bucket] of this.buckets) {
const idx = bucket.value.findIndex((m) => m.id === id);
if (idx !== -1) {
const next = bucket.value.slice();
const existing = next[idx];
if (!existing) continue;
- next[idx] = { ...existing, state };
+ next[idx] = { ...existing, state, routingError };
bucket.value = next;
return;
}
From ded354703c1ab8e88ef5d14f7a5f1aa32fc66620 Mon Sep 17 00:00:00 2001
From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com>
Date: Mon, 6 Jul 2026 13:42:55 -0700
Subject: [PATCH 02/17] fix chat routing error preservation
---
.../src/features/chat/ChatClient.send.test.ts | 63 ++++++++++++++++++-
packages/sdk/src/features/chat/ChatClient.ts | 3 +-
2 files changed, 64 insertions(+), 2 deletions(-)
diff --git a/packages/sdk/src/features/chat/ChatClient.send.test.ts b/packages/sdk/src/features/chat/ChatClient.send.test.ts
index 2af75d095..2ee2e0bfa 100644
--- a/packages/sdk/src/features/chat/ChatClient.send.test.ts
+++ b/packages/sdk/src/features/chat/ChatClient.send.test.ts
@@ -1,6 +1,7 @@
+import { create, toBinary } from "@bufbuild/protobuf";
import type { ResultType } from "better-result";
import * as Protobuf from "@meshtastic/protobufs";
-import { describe, expect, it } from "vitest";
+import { describe, expect, it, vi } from "vitest";
import { MeshClient } from "../../core/client/MeshClient.ts";
import { createFakeTransport } from "../../core/testing/createFakeTransport.ts";
import { ChannelNumber } from "../../core/types.ts";
@@ -257,4 +258,64 @@ describe("ChatClient.send optimistic append", () => {
Protobuf.Mesh.Routing_Error.MAX_RETRANSMIT,
);
});
+
+ it("preserves routing error when decoder rejects the queued send afterward", async () => {
+ const { transport, respond } = createFakeTransport();
+ const client = new MeshClient({ transport });
+ const messages = client.chat.messages(ChannelNumber.Primary);
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
+
+ try {
+ const sendPromise = client.chat.send({
+ text: "missing channel",
+ destination: "broadcast",
+ channel: ChannelNumber.Primary,
+ });
+ const packetId = messages.value[0]?.id;
+ if (packetId === undefined) {
+ throw new Error("optimistic message was not appended");
+ }
+ const queued = client.queue
+ .getState()
+ .find((item) => item.id === packetId);
+ if (queued === undefined) {
+ throw new Error("send was not queued");
+ }
+ void queued.promise.catch(() => {});
+
+ const routingPacket = create(Protobuf.Mesh.RoutingSchema, {
+ variant: {
+ case: "errorReason",
+ value: Protobuf.Mesh.Routing_Error.NO_CHANNEL,
+ },
+ });
+ respond.withMeshPacket(
+ create(Protobuf.Mesh.MeshPacketSchema, {
+ id: 654,
+ from: 0,
+ to: client.myNodeNum,
+ channel: ChannelNumber.Primary,
+ rxTime: Math.trunc(Date.now() / 1000),
+ payloadVariant: {
+ case: "decoded",
+ value: {
+ portnum: Protobuf.Portnums.PortNum.ROUTING_APP,
+ requestId: packetId,
+ payload: toBinary(Protobuf.Mesh.RoutingSchema, routingPacket),
+ },
+ },
+ }),
+ );
+
+ const result = await sendPromise;
+ expect(result.status).toBe("error");
+ expect(messages.value).toHaveLength(1);
+ expect(messages.value[0]!.state).toBe(MessageState.Failed);
+ expect(messages.value[0]!.routingError).toBe(
+ Protobuf.Mesh.Routing_Error.NO_CHANNEL,
+ );
+ } finally {
+ errorSpy.mockRestore();
+ }
+ });
});
diff --git a/packages/sdk/src/features/chat/ChatClient.ts b/packages/sdk/src/features/chat/ChatClient.ts
index e3993a69f..94a0c6e9a 100644
--- a/packages/sdk/src/features/chat/ChatClient.ts
+++ b/packages/sdk/src/features/chat/ChatClient.ts
@@ -250,10 +250,11 @@ export class ChatClient {
// Send pipeline failed (transport closed, validation, etc.). Keep
// the optimistic message visible but mark it Failed so the user
// sees the error state next to their bubble.
+ const existing = this.store.findMessage(packetId);
const routingError =
result.error instanceof MessageTooLongError
? Protobuf.Mesh.Routing_Error.TOO_LARGE
- : undefined;
+ : existing?.routingError;
this.store.updateState(packetId, MessageState.Failed, routingError);
void this.repository
.updateState(packetId, MessageState.Failed, routingError)
From d380c1e2b85970e61968096a6260b3b19da8bfb2 Mon Sep 17 00:00:00 2001
From: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com>
Date: Mon, 6 Jul 2026 13:43:56 -0700
Subject: [PATCH 03/17] Add web message status evidence
---
.../message-status/channel-implicit-ack.png | Bin 0 -> 12156 bytes
.../message-status/dm-implicit-ack.png | Bin 0 -> 14289 bytes
.../message-status/encrypted-send-failed.png | Bin 0 -> 17000 bytes
.../message-status/explicit-ack.png | Bin 0 -> 10819 bytes
.../message-status/message-too-large.png | Bin 0 -> 12342 bytes
validation-artifacts/message-status/no-ack.png | Bin 0 -> 15493 bytes
.../message-status/no-channel.png | Bin 0 -> 10777 bytes
.../message-status/overview.png | Bin 0 -> 146964 bytes
.../recipient-key-unavailable.png | Bin 0 -> 15553 bytes
.../recipient-needs-your-key.png | Bin 0 -> 16739 bytes
.../message-status/sending.png | Bin 0 -> 10237 bytes
11 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 validation-artifacts/message-status/channel-implicit-ack.png
create mode 100644 validation-artifacts/message-status/dm-implicit-ack.png
create mode 100644 validation-artifacts/message-status/encrypted-send-failed.png
create mode 100644 validation-artifacts/message-status/explicit-ack.png
create mode 100644 validation-artifacts/message-status/message-too-large.png
create mode 100644 validation-artifacts/message-status/no-ack.png
create mode 100644 validation-artifacts/message-status/no-channel.png
create mode 100644 validation-artifacts/message-status/overview.png
create mode 100644 validation-artifacts/message-status/recipient-key-unavailable.png
create mode 100644 validation-artifacts/message-status/recipient-needs-your-key.png
create mode 100644 validation-artifacts/message-status/sending.png
diff --git a/validation-artifacts/message-status/channel-implicit-ack.png b/validation-artifacts/message-status/channel-implicit-ack.png
new file mode 100644
index 0000000000000000000000000000000000000000..50ac3a38d4fbb11855cb820c52884f9580188420
GIT binary patch
literal 12156
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
literal 0
HcmV?d00001
diff --git a/validation-artifacts/message-status/dm-implicit-ack.png b/validation-artifacts/message-status/dm-implicit-ack.png
new file mode 100644
index 0000000000000000000000000000000000000000..cb906a4fa2fdbea83c9a95758d31452300c670a9
GIT binary patch
literal 14289
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
IMGgG_FFDNl)Bpeg
literal 0
HcmV?d00001
diff --git a/validation-artifacts/message-status/explicit-ack.png b/validation-artifacts/message-status/explicit-ack.png
new file mode 100644
index 0000000000000000000000000000000000000000..9c17573a08fe1474c1c87e4cf2de418ec54f5868
GIT binary patch
literal 10819
zcmcI~Wn7e9*YAjgf`GKNiYSfJ-3>!G0s<04DxCv}$PGAvbPq#!NDR`Tbcb|z4LQ`n
zzzf{Z@0<_M$8+8xWrmq)Tbuk%^z-ihz>}k1Nr}}})&33ia=RV3u&^-qdFOH8t{m3qcRv(j
z<5HjroNT0FGUnKKp9g>e?+LP6Rti~I^NUzM|9VeAgv-22amalv@IIn0EIJb-vU7#&
zzU2Wr>w~E}Q(rg9KeFbuz_?7BjEszcOn+HfEC+nD|BNPcRXp2<@nQ{-T7_i$k(iZN
z(sAFpC6bjsl)Ny0NWm0}^II(L8!m?AX18Q5no6=+9gK6{
zJ!&D0Yy;z76QLg7-E|SYx;p^?&X|wy&jB=&cM=2up36twpZ{N*GzDQwFp;jUu5;^T
zDN*Cxjd?!K^wu;2`hyp=#J3gA*S~X71rh-b>vCJ5y+}~b0yIAqM10yE!E&dCg1_lR
zLg&3UMU1p`WDA;N5P+K+T#@