diff --git a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h
--- a/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h
+++ b/native/cpp/CommonCpp/DatabaseManagers/DatabaseQueryExecutor.h
@@ -166,6 +166,8 @@
   virtual void markOutboundP2PMessageAsSent(
       std::string messageID,
       std::string deviceID) const = 0;
+  virtual std::vector<std::string>
+  resetOutboundP2PMessagesForDevice(std::string deviceID) const = 0;
   virtual void addInboundP2PMessage(InboundP2PMessage message) const = 0;
   virtual std::vector<InboundP2PMessage> getAllInboundP2PMessage() const = 0;
   virtual void
diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h
--- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h
+++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h
@@ -180,6 +180,8 @@
       std::string ciphertext) const override;
   void markOutboundP2PMessageAsSent(std::string messageID, std::string deviceID)
       const override;
+  std::vector<std::string>
+  resetOutboundP2PMessagesForDevice(std::string deviceID) const override;
   void addInboundP2PMessage(InboundP2PMessage message) const override;
   std::vector<InboundP2PMessage> getAllInboundP2PMessage() const override;
   void
diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp
--- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp
+++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp
@@ -9,6 +9,7 @@
 #include "entities/KeyserverInfo.h"
 #include "entities/LocalMessageInfo.h"
 #include "entities/Metadata.h"
+#include "entities/SQLiteDataConverters.h"
 #include "entities/SyncedMetadataEntry.h"
 #include "entities/UserInfo.h"
 #include <fstream>
@@ -2543,6 +2544,83 @@
   sqlite3_step(preparedSQL);
 }
 
+std::vector<std::string> SQLiteQueryExecutor::resetOutboundP2PMessagesForDevice(
+    std::string deviceID) const {
+  // Query all messages that need to be resent - all message that supports
+  // auto retry or already sent messages.
+  std::string queryMessageIDsToResend =
+      "SELECT message_id "
+      "FROM outbound_p2p_messages "
+      "WHERE device_id = ? AND ( "
+      "  supports_auto_retry = 1 "
+      "  OR (supports_auto_retry = 0 AND status = 'sent') "
+      ");";
+
+  SQLiteStatementWrapper preparedQueryMessageIDsSQL(
+      SQLiteQueryExecutor::getConnection(),
+      queryMessageIDsToResend,
+      "Failed to get all messages to reset");
+
+  bindStringToSQL(deviceID.c_str(), preparedQueryMessageIDsSQL, 1);
+  std::vector<std::string> messageIDs;
+  for (int stepResult = sqlite3_step(preparedQueryMessageIDsSQL);
+       stepResult == SQLITE_ROW;
+       stepResult = sqlite3_step(preparedQueryMessageIDsSQL)) {
+    messageIDs.push_back(getStringFromSQLRow(preparedQueryMessageIDsSQL, 0));
+  }
+
+  // Setting ciphertext to an empty string to make sure this message will be
+  // encrypted again with a new session, update the status, and set
+  // supports_auto_retry to true.
+  // Updating supports_auto_retry to true because those are already sent
+  // messages (from the UI perspective), but the recipient failed to decrypt
+  // so needs to be automatically resent.
+  std::stringstream resetMessagesSQLStream;
+  resetMessagesSQLStream
+      << "UPDATE outbound_p2p_messages "
+      << "SET supports_auto_retry = 1, status = 'persisted', ciphertext = '' "
+      << "WHERE message_id IN " << getSQLStatementArray(messageIDs.size())
+      << ";";
+
+  SQLiteStatementWrapper preparedUpdateSQL(
+      SQLiteQueryExecutor::getConnection(),
+      resetMessagesSQLStream.str(),
+      "Failed to reset messages.");
+
+  for (int i = 0; i < messageIDs.size(); i++) {
+    int bindResult = bindStringToSQL(messageIDs[i], preparedUpdateSQL, i + 1);
+    if (bindResult != SQLITE_OK) {
+      std::stringstream error_message;
+      error_message << "Failed to bind key to SQL statement. Details: "
+                    << sqlite3_errstr(bindResult) << std::endl;
+      sqlite3_finalize(preparedUpdateSQL);
+      throw std::runtime_error(error_message.str());
+    }
+  }
+  sqlite3_step(preparedUpdateSQL);
+
+  // This handles the case of messages that are encrypted (with a malformed
+  // session) but not yet queued on Tunnelbroker. In this case, this message
+  // is not considered to be sent (from the UI perspective),
+  // and supports_auto_retry is not updated.
+  std::string updateCiphertextQuery =
+      "UPDATE outbound_p2p_messages "
+      "SET ciphertext = '', status = 'persisted'"
+      "WHERE device_id = ? "
+      "  AND supports_auto_retry = 0 "
+      "  AND status = 'encrypted';";
+
+  SQLiteStatementWrapper preparedUpdateCiphertextSQL(
+      SQLiteQueryExecutor::getConnection(),
+      updateCiphertextQuery,
+      "Failed to set ciphertext");
+
+  bindStringToSQL(deviceID.c_str(), preparedUpdateCiphertextSQL, 1);
+  sqlite3_step(preparedUpdateCiphertextSQL);
+
+  return messageIDs;
+}
+
 void SQLiteQueryExecutor::addInboundP2PMessage(
     InboundP2PMessage message) const {
   static std::string addMessage =
diff --git a/web/cpp/SQLiteQueryExecutorBindings.cpp b/web/cpp/SQLiteQueryExecutorBindings.cpp
--- a/web/cpp/SQLiteQueryExecutorBindings.cpp
+++ b/web/cpp/SQLiteQueryExecutorBindings.cpp
@@ -307,6 +307,9 @@
       .function(
           "markOutboundP2PMessageAsSent",
           &SQLiteQueryExecutor::markOutboundP2PMessageAsSent)
+      .function(
+          "resetOutboundP2PMessagesForDevice",
+          &SQLiteQueryExecutor::resetOutboundP2PMessagesForDevice)
       .function(
           "addInboundP2PMessage", &SQLiteQueryExecutor::addInboundP2PMessage)
       .function(
diff --git a/web/shared-worker/_generated/comm_query_executor.wasm b/web/shared-worker/_generated/comm_query_executor.wasm
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
GIT binary patch
literal 0
Hc$@<O00001

literal 0
Hc$@<O00001

diff --git a/web/shared-worker/queries/outbound-p2p-message-queries.test.js b/web/shared-worker/queries/outbound-p2p-message-queries.test.js
--- a/web/shared-worker/queries/outbound-p2p-message-queries.test.js
+++ b/web/shared-worker/queries/outbound-p2p-message-queries.test.js
@@ -1,6 +1,9 @@
 // @flow
 
-import type { OutboundP2PMessage } from 'lib/types/sqlite-types.js';
+import {
+  type OutboundP2PMessage,
+  outboundP2PMessageStatuses,
+} from 'lib/types/sqlite-types.js';
 
 import { getDatabaseModule } from '../db-module.js';
 import type { EmscriptenModule } from '../types/module.js';
@@ -175,4 +178,81 @@
   it(`should return undefined when a message with ID doesn't exist`, () => {
     expect(queryExecutor?.getOutboundP2PMessagesByID(['id-5'])).toEqual([]);
   });
+
+  it('should reset messages', () => {
+    const deviceID = 'deviceID';
+    const MSG_TO_RESET_1: OutboundP2PMessage = {
+      messageID: 'reset-1',
+      deviceID,
+      userID: 'user-1',
+      timestamp: '1',
+      plaintext: 'decrypted-1',
+      ciphertext: 'encrypted-1',
+      status: 'encrypted',
+      supportsAutoRetry: true,
+    };
+
+    const MSG_TO_RESET_2: OutboundP2PMessage = {
+      messageID: 'reset-2',
+      deviceID,
+      userID: 'user-1',
+      timestamp: '1',
+      plaintext: 'decrypted-1',
+      ciphertext: 'encrypted-1',
+      status: 'sent',
+      supportsAutoRetry: false,
+    };
+
+    const MSG_NOT_RESET: OutboundP2PMessage = {
+      messageID: 'reset-3',
+      deviceID,
+      userID: 'user-1',
+      timestamp: '3',
+      plaintext: 'decrypted-1',
+      ciphertext: 'encrypted-1',
+      status: 'encrypted',
+      supportsAutoRetry: false,
+    };
+
+    queryExecutor?.addOutboundP2PMessages([
+      MSG_TO_RESET_1,
+      MSG_TO_RESET_2,
+      MSG_NOT_RESET,
+    ]);
+
+    const messageIDs =
+      queryExecutor?.resetOutboundP2PMessagesForDevice(deviceID);
+
+    expect(messageIDs).toEqual([
+      MSG_TO_RESET_1.messageID,
+      MSG_TO_RESET_2.messageID,
+    ]);
+
+    const messagesAfterReset = [
+      {
+        ...MSG_TO_RESET_1,
+        status: outboundP2PMessageStatuses.persisted,
+        ciphertext: '',
+        supportsAutoRetry: true,
+      },
+      {
+        ...MSG_TO_RESET_2,
+        status: outboundP2PMessageStatuses.persisted,
+        ciphertext: '',
+        supportsAutoRetry: true,
+      },
+    ];
+    expect(queryExecutor?.getOutboundP2PMessagesByID(messageIDs ?? [])).toEqual(
+      messagesAfterReset,
+    );
+    expect(
+      queryExecutor?.getOutboundP2PMessagesByID([MSG_NOT_RESET.messageID]),
+    ).toEqual([
+      {
+        ...MSG_NOT_RESET,
+        ciphertext: '',
+        status: outboundP2PMessageStatuses.persisted,
+      },
+    ]);
+  });
 });
diff --git a/web/shared-worker/types/sqlite-query-executor.js b/web/shared-worker/types/sqlite-query-executor.js
--- a/web/shared-worker/types/sqlite-query-executor.js
+++ b/web/shared-worker/types/sqlite-query-executor.js
@@ -190,6 +190,7 @@
     ciphertext: string,
   ): void;
   markOutboundP2PMessageAsSent(messageID: string, deviceID: string): void;
+  resetOutboundP2PMessagesForDevice(deviceID: string): $ReadOnlyArray<string>;
 
   addInboundP2PMessage(message: InboundP2PMessage): void;
   getAllInboundP2PMessage(): $ReadOnlyArray<InboundP2PMessage>;