Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 7875a80e55bfc41b72ae6716ca804d1893175379)
+++ src/services/ActivityPubService.js	(revision 0448e340f39233b65d38066dd72201ee7c8c1c80)
@@ -638,7 +638,7 @@
     for (const m of listOutbox(slug).slice(0, need)) {
       items.push({
-        type: 'sent', outboxId: m.id, to_handle: m.to_handle, in_reply_to: m.in_reply_to,
-        post_slug: m.post_slug, content: m.content, editable: m.editable,
-        language: m.language, created_at: m.created_at,
+        type: 'sent', outboxId: m.id, to_handle: m.to_handle, to_actors: m.to_actors,
+        in_reply_to: m.in_reply_to, post_slug: m.post_slug, content: m.content,
+        editable: m.editable, language: m.language, created_at: m.created_at,
       });
     }
@@ -701,6 +701,16 @@
   if (!it || !CONV_TYPES.has(it.type)) return null;
   if (it.post_slug) return `post:${it.post_slug}`;
-  const who = it.handle || it.to_handle || '';
-  const norm = String(who).trim().toLowerCase().replace(/^@/, '');
+  let who = it.handle || it.to_handle || '';
+  // Een direct bericht kan zonder to_handle in de tabel staan (de handle van de
+  // ontvanger was niet af te leiden). De eerste uit to_actors is dan alsnog de
+  // tegenpartij, en zonder deze terugval kreeg een gesprek dat JIJ begon geen
+  // draad -- precies het geval waarin het meest onlogisch is dat het los blijft.
+  if (!who && it.to_actors) {
+    try {
+      const first = JSON.parse(it.to_actors)[0];
+      if (first) who = deriveHandle(first);
+    } catch { /* geen bruikbare lijst → geen sleutel, het blijft een losse regel */ }
+  }
+  const norm = String(who || '').trim().toLowerCase().replace(/^@/, '');
   return norm ? `actor:${norm}` : null;
 }
@@ -3010,5 +3020,5 @@
   // gaat staan (zie threadKey). Zonder die kolom viel een uitwisseling uit
   // elkaar in "Verzonden" en "Gesprekken".
-  return db.prepare('SELECT id, content, to_handle, to_actor, post_slug, in_reply_to, language, created_at FROM ap_outbox WHERE site_slug = ? ORDER BY created_at DESC')
+  return db.prepare('SELECT id, content, to_handle, to_actor, to_actors, post_slug, in_reply_to, language, created_at FROM ap_outbox WHERE site_slug = ? ORDER BY created_at DESC')
     .all(siteSlug).map((r) => { const c = stripLeadingMentions(r.content); return { ...r, content: c, editable: outboxEditableText(c) }; });
 }
Index: src/views/partials/msg-item.ejs
===================================================================
--- src/views/partials/msg-item.ejs	(revision 7875a80e55bfc41b72ae6716ca804d1893175379)
+++ src/views/partials/msg-item.ejs	(revision 0448e340f39233b65d38066dd72201ee7c8c1c80)
@@ -37,11 +37,14 @@
            var _who = n.name || n.handle || '';
            var _priv0 = (n.visibility === 'followers' || n.visibility === 'direct');
-           // Buckets (klonkt-demo-3jf): Messages = @mentions + private (DM) replies;
-           // Activity = likes/boosts/follows; Moderation = reports; Sent = your
-           // replies; Conversations = every other (public) reply.
-           var _kind = _t === 'sent' ? 'sent'
-             : (_t === 'like' || _t === 'boost' || _t === 'follow' || _t === 'poll_done') ? 'act'
+           // Emmers: Activiteit = likes/boosts/follows/polls, Moderatie =
+           // reports, en al het overige is gesprek. De oude emmers 'msgs' en
+           // 'sent' bestaan niet meer sinds Berichten, Gesprekken en Verzonden
+           // samenvielen -- en dat is geen cosmetische hernoeming: een
+           // gespreksregel die om wat voor reden dan ook geen draad werd (geen
+           // post, geen herkenbare tegenpartij) hield anders een data-kind
+           // waarvoor geen chip meer bestond, en was daarmee onder ELKE chip
+           // behalve Alles onzichtbaar.
+           var _kind = (_t === 'like' || _t === 'boost' || _t === 'follow' || _t === 'poll_done') ? 'act'
              : _t === 'report' ? 'mod'
-             : (_t === 'mention' || _priv0) ? 'msgs'
              : 'conv';
            // Binnen een draad is dit geen zelfstandige regel: de chip filtert op
Index: test/messages-conversations.test.js
===================================================================
--- test/messages-conversations.test.js	(revision 7875a80e55bfc41b72ae6716ca804d1893175379)
+++ test/messages-conversations.test.js	(revision 0448e340f39233b65d38066dd72201ee7c8c1c80)
@@ -112,2 +112,22 @@
   assert.equal(out[0].type, 'sent');
 });
+
+test('een direct bericht zonder to_handle valt terug op to_actors', () => {
+  // Precies het geval waarin het het onlogischst is dat er geen draad ontstaat:
+  // een gesprek dat JIJ begon. Zonder deze terugval bleef het een losse regel.
+  const key = threadKey({
+    type: 'sent',
+    to_actors: JSON.stringify(['https://a.test/users/anna', 'https://b.test/users/bo']),
+    created_at: ts(1),
+  });
+  assert.equal(key, 'actor:anna@a.test');
+  // Rommel in de kolom mag niets omgooien.
+  assert.equal(threadKey({ type: 'sent', to_actors: 'geen json' }), null);
+  assert.equal(threadKey({ type: 'sent', to_actors: '[]' }), null);
+});
+
+test('to_handle wint van to_actors, zodat een draad niet splitst', () => {
+  const a = threadKey({ type: 'sent', to_handle: '@anna@a.test' });
+  const b = threadKey({ type: 'sent', to_handle: '@anna@a.test', to_actors: JSON.stringify(['https://b.test/users/bo']) });
+  assert.equal(a, b, 'dezelfde tegenpartij hoort dezelfde sleutel te geven');
+});
