Index: src/routes/activitypub.js
===================================================================
--- src/routes/activitypub.js	(revision f9b1c5c80031868d2bb245eaeba2df9ed2b53eec)
+++ src/routes/activitypub.js	(revision 6a996686e010db8c4fa0ac7769b1aa3876e21f0e)
@@ -280,6 +280,20 @@
     },
   }));
-  // Newest first over both, so the app can keep treating this as one feed.
-  const items = [...posts, ...messages].sort((a, b) => String(b.published || '').localeCompare(String(a.published || '')));
+  // Your OWN sent notes (replies and direct messages, ap_outbox): without
+  // them a reply existed everywhere except in your own app, Messages showed
+  // half a conversation, and a retry ran into the duplicate guard (Robins
+  // melding, 30-7). Served like the other legs: same shape, one parser.
+  const mine = AP.selfAuthor(base, auth.site);
+  const sent = AP.getSentNotes(base, auth.site, 60).map((n) => ({
+    id: `${n.id}#create`,
+    type: 'Create',
+    actor: me,
+    published: n.published,
+    // The leading mention anchor is addressing, not prose (the DM leg strips
+    // it the same way); the Mention tags built from the full content stay.
+    object: { ...n, content: AP.stripLeadingMentions(n.content), 'shaer:author': mine },
+  }));
+  // Newest first over all legs, so the app can keep treating this as one feed.
+  const items = [...posts, ...messages, ...sent].sort((a, b) => String(b.published || '').localeCompare(String(a.published || '')));
   AP.sendAP(res, {
     '@context': AP.AP_CONTEXT,
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision f9b1c5c80031868d2bb245eaeba2df9ed2b53eec)
+++ src/services/ActivityPubService.js	(revision 6a996686e010db8c4fa0ac7769b1aa3876e21f0e)
@@ -2238,4 +2238,16 @@
 }
 
+// The account's own outbound notes (replies and direct messages) as AS2
+// Notes, newest first. The C2S inbox read serves these alongside the
+// timeline: without them your own reply existed everywhere EXCEPT in your
+// own app (Robins melding, 30-7: "replyen werkt nog niet"; het antwoord
+// stond op de server maar de app kreeg het nooit terug, dus je probeerde
+// het opnieuw en liep in de duplicate-guard).
+export function getSentNotes(base, site, limit = 60) {
+  return db.prepare('SELECT * FROM ap_outbox WHERE site_slug = ? ORDER BY created_at DESC LIMIT ?')
+    .all(site.slug, limit)
+    .map((row) => buildReplyNote(base, site, row));
+}
+
 // Resolve one of our outbound reply Notes by id (for /ap/notes/:id fallback).
 export function getOutboxNote(base, id) {
@@ -2319,5 +2331,5 @@
         }
         if (object.inReplyTo) {
-          const parent = await resolveRemoteNote(c2sIdOf(object.inReplyTo)).catch(() => null);
+          const parent = await resolveRemoteNote(c2sIdOf(object.inReplyTo), { asSlug: site.slug }).catch(() => null);
           if (!parent) return { status: 502, error: 'cannot_resolve_inReplyTo' };
           // The attachments ride along (Robins melding, 30-7: "502
@@ -2359,5 +2371,5 @@
           }
         }
-        const note = await resolveRemoteNote(targetUri).catch(() => null);
+        const note = await resolveRemoteNote(targetUri, { asSlug: site.slug }).catch(() => null);
         const objUri = (note && note.object_uri) || targetUri;
         const authorUri = note && note.actor_uri;
@@ -2397,5 +2409,5 @@
         if (innerType === 'Like' || innerType === 'Announce') {
           const kind = innerType === 'Announce' ? 'unboost' : 'unlike';
-          const note = await resolveRemoteNote(innerTarget).catch(() => null);
+          const note = await resolveRemoteNote(innerTarget, { asSlug: site.slug }).catch(() => null);
           const objUri = (note && note.object_uri) || innerTarget;
           await sendInteraction(site, kind, objUri, note && note.actor_uri);
@@ -2586,7 +2598,11 @@
   // Attachments count toward "the same": two media-only replies share content.
   const mediaJson = media.length ? JSON.stringify(media) : null;
-  const dup = db.prepare('SELECT 1 FROM ap_outbox WHERE site_slug = ? AND IFNULL(in_reply_to, \'\') = ? AND content = ? AND IFNULL(attachments, \'\') = IFNULL(?, \'\') LIMIT 1')
+  // A duplicate is idempotent success, not an error: it answers with the
+  // EXISTING id. Returning without one made the C2S ingest say 502
+  // reply_failed on a double-submit (Robins schermafdruk, 30-7), so a retry
+  // of a reply the app never showed looked like the reply itself failing.
+  const dup = db.prepare('SELECT id FROM ap_outbox WHERE site_slug = ? AND IFNULL(in_reply_to, \'\') = ? AND content = ? AND IFNULL(attachments, \'\') = IFNULL(?, \'\') LIMIT 1')
     .get(site.slug, parent.object_uri || '', content, mediaJson);
-  if (dup) { console.log('[AP] outreply skipped (duplicate)'); return { duplicate: true, delivered: 0 }; }
+  if (dup) { console.log('[AP] outreply skipped (duplicate)'); return { duplicate: true, id: dup.id, delivered: 0 }; }
   const id = crypto.randomUUID();
   iStmts().insO.run(id, site.slug, postId, postSlug || null, parent.object_uri || null, toActorUri, toHandle, content, replyLang, mediaJson);
@@ -2643,12 +2659,18 @@
 // Resolve a remote post URL (any fediverse/Klonkt post) into a reply target.
 // Returns a parent-shaped object usable by deliverReply(), or null.
-export async function resolveRemoteNote(url) {
+export async function resolveRemoteNote(url, opts = {}) {
   if (!/^https?:\/\//i.test(String(url || ''))) return null;
-  const note = await fetchActor(url).catch(() => null); // AP GET (content-negotiates)
+  // With `asSlug` the fetches are SIGNED as that local actor. An anonymous
+  // GET can only read public notes; a friends-only note (Shaer's default!)
+  // rightly refuses it, which made every reply to a friend's post fail while
+  // a reply to your own public post worked (Robins melding, 30-7). Signed,
+  // the other server sees WHO asks and serves what the friendship earns.
+  const get = (u) => (opts.asSlug ? signedGetJson(opts.asSlug, u) : fetchActor(u).catch(() => null));
+  const note = await get(url); // AP GET (content-negotiates)
   if (!note || !note.id) return null;
   const att = note.attributedTo;
   const actorUri = actorUriOf(att);
   if (!actorUri) return null;
-  const actor = await fetchActor(actorUri).catch(() => null);
+  const actor = await get(actorUri);
   const ai = actorInfo(actor, actorUri);
   // Is what we're replying to a post (or a comment) on one of OUR posts? If so,
@@ -2664,9 +2686,9 @@
     const url = typeof cursor === 'string' ? cursor : (cursor && cursor.id);
     if (!url) break;
-    const pn = await fetchActor(url).catch(() => null);
+    const pn = await get(url);
     if (!pn) break;
     const pa = actorUriOf(pn.attributedTo);
     if (pa && pa !== actorUri) {
-      const paDoc = await fetchActor(pa).catch(() => null);
+      const paDoc = await get(pa);
       const inbox = paDoc && ((paDoc.endpoints && paDoc.endpoints.sharedInbox) || paDoc.inbox);
       if (inbox && !seenInbox.has(inbox)) { seenInbox.add(inbox); threadInboxes.push(inbox); }
@@ -4167,5 +4189,5 @@
   buildActor, buildNote, buildCreate, buildOutbox, buildFollowers, buildFollowing, buildFeatured,
   followerCount, deliver, fetchActor, verifyRequest, handleInbox, deliverCreate, deliverDelete, deliverUpdate, deliverActorUpdate, resyncFeaturedPins,
-  getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, deliverReply, resolveRemoteNote,
+  getInteractions, getInteractionById, setInteractionBoosted, setInteractionLiked, setMyReaction, getMyReactions, buildReplyNote, getOutboxNote, getSentNotes, deliverReply, resolveRemoteNote,
   listOutbox, deliverOutboxDelete, deliverOutboxUpdate, deliverDirectNote,
   webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, backfillFromOutbox, getTimeline, getDirectMessages, isoStamp, timelineAttachments, timelineEmojis, timelineObjectLinks, timelineQuote, timelineEmbed, applyQuoteProps, deliverToActor, sendInteraction, voteOnPoll, voteOnRemotePoll,
