diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -2,6 +2,14 @@
 
 ## Unreleased
 
+## 0.3.2.16 - 2026-07-15
+- Host: normalize the random `Id`, item path, and menu path generated by
+  Ayatana StatusNotifierItem implementations when building logical duplicate
+  keys. Multiple instances of the same application now collapse to one tray
+  item even when each instance advertises a different generated identity.
+- Add regression coverage for generated Ayatana identities while preserving
+  distinct identities for ordinary StatusNotifierItems.
+
 ## 0.3.2.15 - 2026-07-02
 - Bound every DBus call made during host and watcher startup with a timeout so
   a single unresponsive item (a process that owns its `StatusNotifierItem` bus
diff --git a/src/StatusNotifier/Host/Service.hs b/src/StatusNotifier/Host/Service.hs
--- a/src/StatusNotifier/Host/Service.hs
+++ b/src/StatusNotifier/Host/Service.hs
@@ -23,6 +23,7 @@
 import Data.Coerce
 import Data.Either
 import Data.Int
+import Data.List (stripPrefix)
 import qualified Data.Map.Strict as Map
 import Data.Maybe
 import Data.String
@@ -133,14 +134,31 @@
 
 logicalDuplicateKey :: ItemInfo -> Maybe LogicalDuplicateKey
 logicalDuplicateKey ItemInfo {..} =
-  let key =
+  let generatedAyatanaIdentity = do
+        suffix <- stripPrefix "/org/ayatana/NotificationItem/" (coerce itemServicePath :: String)
+        normalizedId <- itemId >>= normalizeOptionalString
+        itemMenuPath <- menuPath
+        guard $ normalizedId == suffix
+        guard $ (coerce itemMenuPath :: String) == (coerce itemServicePath :: String) <> "/Menu"
+        pure ()
+      hasGeneratedAyatanaIdentity = isJust generatedAyatanaIdentity
+      key =
         LogicalDuplicateKey
-          { duplicateItemId = itemId >>= normalizeOptionalString,
+          { duplicateItemId =
+              if hasGeneratedAyatanaIdentity
+                then Nothing
+                else itemId >>= normalizeOptionalString,
             duplicateIconTitle = normalizeOptionalString iconTitle,
             duplicateIconName = normalizeOptionalString iconName,
             duplicateCategory = itemCategory >>= normalizeOptionalString,
-            duplicateMenuPath = menuPath,
-            duplicatePath = itemServicePath,
+            duplicateMenuPath =
+              if hasGeneratedAyatanaIdentity
+                then Nothing
+                else menuPath,
+            duplicatePath =
+              if hasGeneratedAyatanaIdentity
+                then objectPath_ "/org/ayatana/NotificationItem"
+                else itemServicePath,
             duplicateIsMenu = itemIsMenu
           }
       hasStableIdentity =
diff --git a/status-notifier-item.cabal b/status-notifier-item.cabal
--- a/status-notifier-item.cabal
+++ b/status-notifier-item.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           status-notifier-item
-version:        0.3.2.15
+version:        0.3.2.16
 synopsis:       A wrapper over the StatusNotifierItem/libappindicator dbus specification
 description:    Please see the README on Github at <https://github.com/taffybar/status-notifier-item#readme>
 category:       Desktop
diff --git a/test/HostSpec.hs b/test/HostSpec.hs
--- a/test/HostSpec.hs
+++ b/test/HostSpec.hs
@@ -33,6 +33,49 @@
 
 spec :: Spec
 spec = around withIsolatedSessionBus $ do
+  describe "logicalDuplicateKey" $ do
+    it "normalizes generated Ayatana identities so duplicate app instances collapse" $ \() -> do
+      let localSendItem token =
+            ItemInfo
+              { itemServiceName = busName_ ("org.test.LocalSend." <> token),
+                itemServicePath = objectPath_ ("/org/ayatana/NotificationItem/" <> token),
+                itemId = Just token,
+                itemStatus = Just "Active",
+                itemCategory = Just "ApplicationStatus",
+                itemToolTip = Nothing,
+                iconTitle = "localsend_app",
+                iconName = "/nix/store/example-localsend/logo.png",
+                overlayIconName = Nothing,
+                iconThemePath = Nothing,
+                iconPixmaps = [],
+                overlayIconPixmaps = [],
+                menuPath = Just $ objectPath_ ("/org/ayatana/NotificationItem/" <> token <> "/Menu"),
+                itemIsMenu = True
+              }
+      logicalDuplicateKey (localSendItem "generatedA")
+        `shouldBe` logicalDuplicateKey (localSendItem "generatedB")
+
+    it "retains non-Ayatana item ids as part of logical identity" $ \() -> do
+      let itemWithId itemIdValue =
+            ItemInfo
+              { itemServiceName = busName_ "org.test.RegularItem",
+                itemServicePath = objectPath_ defaultPath,
+                itemId = Just itemIdValue,
+                itemStatus = Just "Active",
+                itemCategory = Just "ApplicationStatus",
+                itemToolTip = Nothing,
+                iconTitle = "shared-title",
+                iconName = "shared-icon",
+                overlayIconName = Nothing,
+                iconThemePath = Nothing,
+                iconPixmaps = [],
+                overlayIconPixmaps = [],
+                menuPath = Nothing,
+                itemIsMenu = False
+              }
+      logicalDuplicateKey (itemWithId "first")
+        `shouldNotBe` logicalDuplicateKey (itemWithId "second")
+
   describe "StatusNotifier.Host.Service integration" $ do
     it "receives ItemAdded then ItemRemoved for a registered item" $ \() -> do
       _watcher <- startWatcher
