diff --git a/app/src/agentworld/pages/MessagingSection.test.tsx b/app/src/agentworld/pages/MessagingSection.test.tsx
index 909c230ac..64eba9ece 100644
--- a/app/src/agentworld/pages/MessagingSection.test.tsx
+++ b/app/src/agentworld/pages/MessagingSection.test.tsx
@@ -279,6 +279,76 @@ describe('DMs panel (E2E enabled)', () => {
// The messages namespace has no 'send' method — only signal.sendMessage is callable
});
+ test('shows a friendly error when the recipient has no published key bundle', async () => {
+ const user = userEvent.setup();
+ vi.mocked(apiClient.messages.list).mockResolvedValue({ messages: [] });
+ vi.mocked(apiClient.signal.sendMessage).mockRejectedValueOnce(
+ new Error(
+ 'CoreRpcError: HTTP 404: HTTP 404: /keys/61KcG5aGLqpnJz2fn4tujFKAdzqsdGR9XqiUeVoT3vPg/bundle: not found'
+ )
+ );
+
+ render();
+ const peerInput = screen.getByPlaceholderText(/Recipient @handle/);
+ await user.type(peerInput, 'peer456');
+ await user.click(screen.getByRole('button', { name: 'Open DM' }));
+
+ const composeInput = await screen.findByPlaceholderText(/Type a message/);
+ await user.type(composeInput, 'secret');
+ await user.click(screen.getByRole('button', { name: 'Send' }));
+
+ expect(
+ await screen.findByText(/This user hasn't enabled encrypted messaging yet/)
+ ).toBeInTheDocument();
+ expect(screen.queryByText(/CoreRpcError/)).not.toBeInTheDocument();
+ expect(screen.queryByText(/\/keys\//)).not.toBeInTheDocument();
+ });
+
+ test('normalizes the core mapped missing encrypted messaging setup error', async () => {
+ const user = userEvent.setup();
+ vi.mocked(apiClient.messages.list).mockResolvedValue({ messages: [] });
+ vi.mocked(apiClient.signal.sendMessage).mockRejectedValueOnce(
+ new Error(
+ 'CoreRpcError: Recipient has not set up encrypted messaging yet. They need to provision Signal keys before receiving DMs. (resolved agent_id: resolved-crypto-id)'
+ )
+ );
+
+ render();
+ const peerInput = screen.getByPlaceholderText(/Recipient @handle/);
+ await user.type(peerInput, 'peer456');
+ await user.click(screen.getByRole('button', { name: 'Open DM' }));
+
+ const composeInput = await screen.findByPlaceholderText(/Type a message/);
+ await user.type(composeInput, 'secret');
+ await user.click(screen.getByRole('button', { name: 'Send' }));
+
+ expect(
+ await screen.findByText(/This user hasn't enabled encrypted messaging yet/)
+ ).toBeInTheDocument();
+ expect(screen.queryByText(/CoreRpcError/)).not.toBeInTheDocument();
+ expect(screen.queryByText(/resolved agent_id/)).not.toBeInTheDocument();
+ });
+
+ test('normalizes missing key bundle errors while refreshing direct messages', async () => {
+ const user = userEvent.setup();
+ vi.mocked(apiClient.messages.list).mockRejectedValueOnce(
+ new Error(
+ 'CoreRpcError: HTTP 404: HTTP 404: /keys/61KcG5aGLqpnJz2fn4tujFKAdzqsdGR9XqiUeVoT3vPg/bundle: not found'
+ )
+ );
+
+ render();
+ const peerInput = screen.getByPlaceholderText(/Recipient @handle/);
+ await user.type(peerInput, 'peer456');
+ await user.click(screen.getByRole('button', { name: 'Open DM' }));
+
+ expect(
+ await screen.findByText(/This user hasn't enabled encrypted messaging yet/)
+ ).toBeInTheDocument();
+ expect(screen.queryByText(/CoreRpcError/)).not.toBeInTheDocument();
+ expect(screen.queryByText(/\/keys\//)).not.toBeInTheDocument();
+ });
+
test('received messages are decrypted before display', async () => {
// directory.resolve returns 'resolved-crypto-id'; messages must use that id
// in the 'from' field to survive the peerId filter in useDirectMessages.
diff --git a/app/src/agentworld/pages/MessagingSection.tsx b/app/src/agentworld/pages/MessagingSection.tsx
index 42ce33c50..9cf4e7551 100644
--- a/app/src/agentworld/pages/MessagingSection.tsx
+++ b/app/src/agentworld/pages/MessagingSection.tsx
@@ -27,6 +27,7 @@ import {
PaymentRequiredError,
type SignalKeyStatus,
} from '../../lib/agentworld/invokeApiClient';
+import { useT } from '../../lib/i18n/I18nContext';
import { fetchWalletStatus } from '../../services/walletApi';
import { apiClient } from '../AgentWorldShell';
import { useTinyplaceStream } from '../hooks/useTinyplaceStream';
@@ -1049,7 +1050,21 @@ interface DecryptedMessage {
outgoing?: boolean;
}
-function useDirectMessages(peerId: string) {
+function formatDirectMessageError(err: unknown, missingBundleMessage: string): string {
+ const message = String(err);
+ const rawBundle404 = /http\s*404/i.test(message) && /\/keys\/[^/\s]+\/bundle\b/i.test(message);
+ const mappedMissingBundle =
+ /recipient/i.test(message) &&
+ /not\s+set\s+up/i.test(message) &&
+ /encrypted\s+messaging/i.test(message);
+ if (rawBundle404 || mappedMissingBundle) {
+ log('[agentworld:dm] recipient key bundle missing');
+ return missingBundleMessage;
+ }
+ return message;
+}
+
+function useDirectMessages(peerId: string, missingBundleMessage: string) {
const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
@@ -1086,11 +1101,11 @@ function useDirectMessages(peerId: string) {
return merged.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
});
} catch (err) {
- setError(String(err));
+ setError(formatDirectMessageError(err, missingBundleMessage));
} finally {
setLoading(false);
}
- }, [peerId]);
+ }, [missingBundleMessage, peerId]);
const send = useCallback(
async (plaintext: string) => {
@@ -1113,12 +1128,12 @@ function useDirectMessages(peerId: string) {
);
await refresh();
} catch (err) {
- setError(String(err));
+ setError(formatDirectMessageError(err, missingBundleMessage));
} finally {
setSending(false);
}
},
- [peerId, refresh]
+ [missingBundleMessage, peerId, refresh]
);
useEffect(() => {
@@ -1139,7 +1154,15 @@ function ActiveDmView({
composeText: string;
setComposeText: (v: string) => void;
}) {
- const { messages, loading, error, sending, send } = useDirectMessages(peerId);
+ const { t } = useT();
+ const missingBundleMessage = t(
+ 'agentworld.messaging.missingSignalBundle',
+ "This user hasn't enabled encrypted messaging yet. Ask them to open Agent World and enable secure DMs before sending a message."
+ );
+ const { messages, loading, error, sending, send } = useDirectMessages(
+ peerId,
+ missingBundleMessage
+ );
const handleSend = useCallback(async () => {
if (!composeText.trim()) return;
diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts
index 9edd7f082..a1f8caab7 100644
--- a/app/src/lib/i18n/ar.ts
+++ b/app/src/lib/i18n/ar.ts
@@ -5774,6 +5774,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'إلغاء',
'agentworld.jobs.applyModal.submit': 'إرسال الطلب',
'agentworld.jobs.applyModal.submitting': 'جارٍ التقديم…',
+ 'agentworld.messaging.missingSignalBundle':
+ 'لم يفعّل هذا المستخدم الرسائل المشفرة بعد. اطلب منه فتح Agent World وتفعيل الرسائل المباشرة الآمنة قبل إرسال رسالة.',
};
export default messages;
diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts
index f51cd5aad..d8bf168d0 100644
--- a/app/src/lib/i18n/bn.ts
+++ b/app/src/lib/i18n/bn.ts
@@ -5891,6 +5891,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'বাতিল',
'agentworld.jobs.applyModal.submit': 'আবেদন জমা দিন',
'agentworld.jobs.applyModal.submitting': 'আবেদন করা হচ্ছে…',
+ 'agentworld.messaging.missingSignalBundle':
+ 'এই ব্যবহারকারী এখনো এনক্রিপ্টেড মেসেজিং চালু করেননি। বার্তা পাঠানোর আগে তাকে Agent World খুলে নিরাপদ DM চালু করতে বলুন।',
};
export default messages;
diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts
index 3f30a900e..606a9c735 100644
--- a/app/src/lib/i18n/de.ts
+++ b/app/src/lib/i18n/de.ts
@@ -6055,6 +6055,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Abbrechen',
'agentworld.jobs.applyModal.submit': 'Bewerbung einreichen',
'agentworld.jobs.applyModal.submitting': 'Wird eingereicht…',
+ 'agentworld.messaging.missingSignalBundle':
+ 'Dieser Benutzer hat verschlüsselte Nachrichten noch nicht aktiviert. Bitte ihn, Agent World zu öffnen und sichere DMs zu aktivieren, bevor du eine Nachricht sendest.',
};
export default messages;
diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts
index 049ff6b09..42b2842e5 100644
--- a/app/src/lib/i18n/en.ts
+++ b/app/src/lib/i18n/en.ts
@@ -6160,6 +6160,8 @@ const en: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Cancel',
'agentworld.jobs.applyModal.submit': 'Submit Application',
'agentworld.jobs.applyModal.submitting': 'Applying…',
+ 'agentworld.messaging.missingSignalBundle':
+ "This user hasn't enabled encrypted messaging yet. Ask them to open Agent World and enable secure DMs before sending a message.",
};
export default en;
diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts
index 26f97b5f1..9f72c54d8 100644
--- a/app/src/lib/i18n/es.ts
+++ b/app/src/lib/i18n/es.ts
@@ -6015,6 +6015,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Cancelar',
'agentworld.jobs.applyModal.submit': 'Enviar solicitud',
'agentworld.jobs.applyModal.submitting': 'Enviando…',
+ 'agentworld.messaging.missingSignalBundle':
+ 'Este usuario aún no ha activado la mensajería cifrada. Pídele que abra Agent World y active los DM seguros antes de enviarle un mensaje.',
};
export default messages;
diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts
index c56135b04..cde0bc198 100644
--- a/app/src/lib/i18n/fr.ts
+++ b/app/src/lib/i18n/fr.ts
@@ -6033,6 +6033,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Annuler',
'agentworld.jobs.applyModal.submit': 'Soumettre la candidature',
'agentworld.jobs.applyModal.submitting': 'Envoi en cours…',
+ 'agentworld.messaging.missingSignalBundle':
+ "Cet utilisateur n'a pas encore activé la messagerie chiffrée. Demandez-lui d'ouvrir Agent World et d'activer les messages privés sécurisés avant d'envoyer un message.",
};
export default messages;
diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts
index 5a6d0ee0c..6e73f84dd 100644
--- a/app/src/lib/i18n/hi.ts
+++ b/app/src/lib/i18n/hi.ts
@@ -5893,6 +5893,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'रद्द करें',
'agentworld.jobs.applyModal.submit': 'आवेदन सबमिट करें',
'agentworld.jobs.applyModal.submitting': 'आवेदन हो रहा है…',
+ 'agentworld.messaging.missingSignalBundle':
+ 'इस उपयोगकर्ता ने अभी तक एन्क्रिप्टेड मैसेजिंग चालू नहीं की है। संदेश भेजने से पहले उनसे Agent World खोलकर सुरक्षित DM चालू करने को कहें।',
};
export default messages;
diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts
index 3f4f921ad..71551410c 100644
--- a/app/src/lib/i18n/id.ts
+++ b/app/src/lib/i18n/id.ts
@@ -5909,6 +5909,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Batal',
'agentworld.jobs.applyModal.submit': 'Kirim Lamaran',
'agentworld.jobs.applyModal.submitting': 'Melamar…',
+ 'agentworld.messaging.missingSignalBundle':
+ 'Pengguna ini belum mengaktifkan pesan terenkripsi. Minta mereka membuka Agent World dan mengaktifkan DM aman sebelum Anda mengirim pesan.',
};
export default messages;
diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts
index 4dd0cab6e..f6c1f0fea 100644
--- a/app/src/lib/i18n/it.ts
+++ b/app/src/lib/i18n/it.ts
@@ -6002,6 +6002,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Annulla',
'agentworld.jobs.applyModal.submit': 'Invia candidatura',
'agentworld.jobs.applyModal.submitting': 'Candidatura in corso…',
+ 'agentworld.messaging.missingSignalBundle':
+ 'Questo utente non ha ancora attivato la messaggistica crittografata. Chiedigli di aprire Agent World e attivare i DM sicuri prima di inviare un messaggio.',
};
export default messages;
diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts
index 1ebccf6c9..32224c6b2 100644
--- a/app/src/lib/i18n/ko.ts
+++ b/app/src/lib/i18n/ko.ts
@@ -5838,6 +5838,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': '취소',
'agentworld.jobs.applyModal.submit': '지원서 제출',
'agentworld.jobs.applyModal.submitting': '지원 중…',
+ 'agentworld.messaging.missingSignalBundle':
+ '이 사용자는 아직 암호화 메시지를 활성화하지 않았습니다. 메시지를 보내기 전에 Agent World를 열고 보안 DM을 활성화해 달라고 요청하세요.',
};
export default messages;
diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts
index f4322eddf..581aeac2f 100644
--- a/app/src/lib/i18n/pl.ts
+++ b/app/src/lib/i18n/pl.ts
@@ -5983,6 +5983,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Anuluj',
'agentworld.jobs.applyModal.submit': 'Wyślij aplikację',
'agentworld.jobs.applyModal.submitting': 'Wysyłanie…',
+ 'agentworld.messaging.missingSignalBundle':
+ 'Ten użytkownik nie włączył jeszcze szyfrowanych wiadomości. Poproś go, aby otworzył Agent World i włączył bezpieczne DM przed wysłaniem wiadomości.',
};
export default messages;
diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts
index 29ca49a43..8f6c685cb 100644
--- a/app/src/lib/i18n/pt.ts
+++ b/app/src/lib/i18n/pt.ts
@@ -5995,6 +5995,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Cancelar',
'agentworld.jobs.applyModal.submit': 'Enviar candidatura',
'agentworld.jobs.applyModal.submitting': 'A enviar…',
+ 'agentworld.messaging.missingSignalBundle':
+ 'Este utilizador ainda não ativou as mensagens encriptadas. Peça-lhe para abrir o Agent World e ativar DMs seguras antes de enviar uma mensagem.',
};
export default messages;
diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts
index f08941a8c..ba0b7470e 100644
--- a/app/src/lib/i18n/ru.ts
+++ b/app/src/lib/i18n/ru.ts
@@ -5956,6 +5956,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': 'Отмена',
'agentworld.jobs.applyModal.submit': 'Отправить заявку',
'agentworld.jobs.applyModal.submitting': 'Отправка…',
+ 'agentworld.messaging.missingSignalBundle':
+ 'Этот пользователь еще не включил зашифрованные сообщения. Попросите его открыть Agent World и включить безопасные личные сообщения перед отправкой.',
};
export default messages;
diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts
index 966aee843..7fe79fdba 100644
--- a/app/src/lib/i18n/zh-CN.ts
+++ b/app/src/lib/i18n/zh-CN.ts
@@ -5599,6 +5599,8 @@ const messages: TranslationMap = {
'agentworld.jobs.applyModal.cancel': '取消',
'agentworld.jobs.applyModal.submit': '提交申请',
'agentworld.jobs.applyModal.submitting': '申请中…',
+ 'agentworld.messaging.missingSignalBundle':
+ '此用户尚未启用加密消息。发送消息前,请让对方打开 Agent World 并启用安全私信。',
};
export default messages;