Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 78b8e58cb0313999935f4f36dcaaf2d5d90342f1)
+++ src/config/database.js	(revision 76290bf80c10aee5bb719e7e1e1c056970316e3e)
@@ -834,4 +834,5 @@
       content TEXT,                 -- sanitized HTML snippet of the mentioning note
       published TEXT,
+      in_reply_to TEXT,             -- de note waarop dit een antwoord is (AS2 inReplyTo)
       created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
       UNIQUE(slug, object_uri)
@@ -987,4 +988,10 @@
   ensureColumn('ap_mentions', 'quote_json', 'TEXT');        // FEP-044f quoted post
   ensureColumn('ap_mentions', 'embed_json', 'TEXT');        // external link preview
+  // AS2 inReplyTo op een vermelding/bericht. Bestond hier niet, en daarmee ging
+  // de ouder bij het opslaan verloren: elk ANTWOORD in een gesprek kwam bij de
+  // client aan alsof het een gesprek begon (Robins melding, 26-8). Bestaande
+  // rijen blijven leeg -- die ouder is niet meer te achterhalen zonder hem
+  // opnieuw op te halen, en een verzonnen ouder is erger dan geen.
+  ensureColumn('ap_mentions', 'in_reply_to', 'TEXT');
   ensureColumn('ap_interactions', 'media_json', 'TEXT');
   ensureColumn('ap_interactions', 'quote_json', 'TEXT');
Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision 78b8e58cb0313999935f4f36dcaaf2d5d90342f1)
+++ src/routes/activitypub.js	(revision 76290bf80c10aee5bb719e7e1e1c056970316e3e)
@@ -543,4 +543,10 @@
       content: AP.stripLeadingMentions(m.content),
       url: m.note_url || undefined,
+      // Waar dit een antwoord op is (Robins melding, 26-8). Zonder dit veld
+      // kwam elk antwoord binnen als het begin van een gesprek: de client kan
+      // een keten alleen teruglopen langs inReplyTo. De andere twee legs
+      // (replyItem uit ap_interactions, sentItem via buildNote) droegen hem
+      // al -- deze was de enige die hem niet eens opsloeg.
+      inReplyTo: m.in_reply_to || undefined,
       published: AP.isoStamp(m.published || m.created_at),
       // Addressed to us and to nobody we know of: the other recipients of a
Index: src/services/ap-inbox.js
===================================================================
--- src/services/ap-inbox.js	(revision 78b8e58cb0313999935f4f36dcaaf2d5d90342f1)
+++ src/services/ap-inbox.js	(revision 76290bf80c10aee5bb719e7e1e1c056970316e3e)
@@ -691,7 +691,18 @@
         for (const slug of slugs) {
           try {
-            const r = db.prepare(`INSERT OR IGNORE INTO ap_mentions (slug, object_uri, note_url, actor_uri, actor_name, actor_handle, actor_icon, actor_url, content, published, help_request, wave, has_guardians, emoji_json, actor_emoji_json, media_json, created_at)
-                                  VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)`)
-              .run(slug, o.id, safeUrl(o.url) || null, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.published || null, help ? 1 : 0, wave ? 1 : 0, hasG ? 1 : 0,
+            // De OUDER gaat mee (Robins melding, 26-8). Hij stond nergens in
+            // deze rij, dus een antwoord binnen een gesprek kwam bij de client
+            // aan alsof het een gesprek begon: de app kan een keten alleen
+            // teruglopen langs inReplyTo, en die was leeg.
+            //
+            // Alleen een http(s)-adres, langs dezelfde poort als `url`: een
+            // inReplyTo komt van een vreemde en mag geen ander schema
+            // binnensmokkelen. AS2 staat een string of een object toe, dus
+            // allebei uitpakken -- alleen de string erkennen zou hetzelfde gat
+            // laten voor iedereen die de objectvorm stuurt.
+            const ouder = safeUrl(typeof o.inReplyTo === 'string' ? o.inReplyTo : (o.inReplyTo && o.inReplyTo.id)) || null;
+            const r = db.prepare(`INSERT OR IGNORE INTO ap_mentions (slug, object_uri, note_url, actor_uri, actor_name, actor_handle, actor_icon, actor_url, content, published, in_reply_to, help_request, wave, has_guardians, emoji_json, actor_emoji_json, media_json, created_at)
+                                  VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,CURRENT_TIMESTAMP)`)
+              .run(slug, o.id, safeUrl(o.url) || null, actorUri, ai.name, ai.handle, ai.icon, ai.url, html, o.published || null, ouder, help ? 1 : 0, wave ? 1 : 0, hasG ? 1 : 0,
                 extractEmojiTags(o.tag), emojiJsonOf(ai.emojis), mediaFromNote(o));
             if (r.changes) {
Index: src/services/ap-timeline.js
===================================================================
--- src/services/ap-timeline.js	(revision 78b8e58cb0313999935f4f36dcaaf2d5d90342f1)
+++ src/services/ap-timeline.js	(revision 76290bf80c10aee5bb719e7e1e1c056970316e3e)
@@ -334,5 +334,5 @@
 const MESSAGE_COLUMNS = `
       m.object_uri, m.note_url, m.actor_uri, m.actor_name, m.actor_handle, m.actor_icon, m.actor_url,
-      m.content, m.published, m.created_at, m.wave, m.help_request,
+      m.content, m.published, m.created_at, m.wave, m.help_request, m.in_reply_to,
       m.emoji_json, m.actor_emoji_json, m.media_json, m.quote_json, m.embed_json`;
 
Index: test/mention-inreplyto.test.js
===================================================================
--- test/mention-inreplyto.test.js	(revision 76290bf80c10aee5bb719e7e1e1c056970316e3e)
+++ test/mention-inreplyto.test.js	(revision 76290bf80c10aee5bb719e7e1e1c056970316e3e)
@@ -0,0 +1,78 @@
+// Een ANTWOORD dat als vermelding/bericht binnenkomt hoort zijn ouder te
+// houden (Robins melding, 26-8). Dat deed het niet: ap_mentions had geen
+// kolom voor inReplyTo, dus de ouder viel bij het opslaan op de grond en de
+// C2S-lezing kon hem niet serveren. Een client kan een gesprek alleen
+// teruglopen langs inReplyTo, dus elk antwoord kwam aan als het BEGIN van een
+// gesprek.
+//
+// Getoetst op de keten, niet op een kolom: binnen via handleInbox, eruit via
+// dezelfde leesweg die de app gebruikt.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://test.example';
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+{ const stil = console.log; console.log = () => {}; try { dbMod.initializeDatabase(); } finally { console.log = stil; } }
+const AP = (await import('../src/services/ActivityPubService.js')).default;
+
+db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+  .run('u1', 'robin', 'u1@t', 'x', 'god');
+db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)').run('s1', 'kid', 'kid', 'u1');
+
+const mij = 'https://test.example/ap/users/kid';
+
+/** Een binnenkomende directe note, zoals de inbox hem van de andere kant krijgt. */
+async function binnen(note) {
+  const act = {
+    '@context': 'https://www.w3.org/ns/activitystreams',
+    id: `${note.id}#create`, type: 'Create', actor: note.attributedTo, object: note,
+  };
+  // preVerified: de handtekening zelf is hier niet het onderwerp, en zonder
+  // netwerk valt er niets op te halen.
+  await AP.handleInbox(
+    { body: act, headers: {}, method: 'POST', originalUrl: '/ap/users/kid/inbox', rawBody: Buffer.from(JSON.stringify(act)) },
+    'kid',
+    { id: note.attributedTo },
+  );
+}
+
+const note = (id, extra = {}) => ({
+  id, type: 'Note', attributedTo: 'https://pruts.nl/ap/users/bart',
+  content: `<p>${id}</p>`, to: [mij],
+  tag: [{ type: 'Mention', href: mij, name: '@kid@test.example' }],
+  published: '2026-08-26T10:00:00Z',
+  ...extra,
+});
+
+test('een binnengekomen antwoord houdt zijn ouder, van opslag tot lezing', async () => {
+  await binnen(note('https://pruts.nl/notes/1'));
+  await binnen(note('https://pruts.nl/notes/2', { inReplyTo: 'https://pruts.nl/notes/1' }));
+
+  const berichten = AP.getDirectMessages('kid', 20);
+  const kop = berichten.find((m) => m.object_uri === 'https://pruts.nl/notes/2');
+  const eerste = berichten.find((m) => m.object_uri === 'https://pruts.nl/notes/1');
+  assert.ok(kop && eerste, 'allebei de berichten kwamen binnen');
+  assert.equal(kop.in_reply_to, 'https://pruts.nl/notes/1', 'het antwoord draagt zijn ouder');
+  assert.equal(eerste.in_reply_to, null, 'en wie niets beantwoordt heeft er geen');
+
+  // Dezelfde rij zoals een gesprek hem opvraagt: een keten teruglopen gebeurt
+  // langs deze weg, niet langs de inbox-lijst.
+  const [uitGesprek] = AP.messageRowsByUri('kid', ['https://pruts.nl/notes/2']);
+  assert.equal(uitGesprek.in_reply_to, 'https://pruts.nl/notes/1', 'ook via de gesprekslezing');
+});
+
+test('de ouder mag alleen een http(s)-adres zijn, in beide AS2-vormen', async () => {
+  // AS2 staat een string of een object toe. Alleen de string erkennen laat
+  // hetzelfde gat open voor iedereen die de objectvorm stuurt.
+  await binnen(note('https://pruts.nl/notes/3', { inReplyTo: { id: 'https://pruts.nl/notes/1', type: 'Note' } }));
+  const [obj] = AP.messageRowsByUri('kid', ['https://pruts.nl/notes/3']);
+  assert.equal(obj.in_reply_to, 'https://pruts.nl/notes/1', 'de objectvorm telt net zo goed');
+
+  // En een vreemde mag langs dit veld geen ander schema binnensmokkelen.
+  await binnen(note('https://pruts.nl/notes/4', { inReplyTo: 'javascript:alert(1)' }));
+  const [vies] = AP.messageRowsByUri('kid', ['https://pruts.nl/notes/4']);
+  assert.equal(vies.in_reply_to, null, 'geen javascript:-adres in de kolom');
+});
