Index: src/routes/posts.js
===================================================================
--- src/routes/posts.js	(revision 6dd1a3f0ef839a8ce97dec8cc96e5b00ae93a8bc)
+++ src/routes/posts.js	(revision 14f54a7a8347f9521ad9b1f426f36b48b00993d6)
@@ -867,5 +867,15 @@
     on = !ActivityPubService.getTimelineReaction(site.slug, note).boosted;
     try { await ActivityPubService.sendInteraction(site, on ? 'boost' : 'unboost', note, (req.body.author || '').toString()); } catch (e) { /* ignore */ }
-    if (on) ActivityPubService.markBoosted(site.slug, note); else ActivityPubService.unmarkBoosted(site.slug, note);
+    if (on) {
+      ActivityPubService.markBoosted(site.slug, note); // instant UI state
+      // Fire-and-forget: re-resolve the note so the cached row is refreshed
+      // (cover/content) — boosting again heals a stale copy from EVERY boost
+      // path, not just the interact page.
+      ActivityPubService.resolveRemoteNote(note)
+        .then((n) => { if (n) ActivityPubService.upsertBoostedNote(site.slug, n); })
+        .catch(() => { /* best-effort */ });
+    } else {
+      ActivityPubService.unmarkBoosted(site.slug, note);
+    }
   }
   if (req.get('X-Requested-With') === 'fetch') return res.json({ ok: true, on });
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 6dd1a3f0ef839a8ce97dec8cc96e5b00ae93a8bc)
+++ src/services/ActivityPubService.js	(revision 14f54a7a8347f9521ad9b1f426f36b48b00993d6)
@@ -1878,5 +1878,5 @@
 // during a flux window, e.g. a fleet-wide update), and drops notes that are gone
 // (404/410). Bump SELFHEAL_VERSION only on a release that warrants a re-sync.
-const SELFHEAL_VERSION = 6; // v6: re-fetch so boosted notes cached coverless (pre image-fallback in resolveRemoteNote) pick up their cover
+const SELFHEAL_VERSION = 7; // v7: rerun v6 with retry-until-clean semantics (origins briefly offline no longer stay stale forever)
 async function fetchNoteAP(url) {
   try {
@@ -2052,10 +2052,10 @@
     let rows = [];
     try { rows = db.prepare('SELECT id, content, media_json, nsfw, cw, url FROM ap_timeline ORDER BY rowid DESC LIMIT 200').all(); } catch { /* no table */ }
-    let healed = 0;
+    let healed = 0, failed = 0;
     for (const r of rows) {
       try {
         const note = await fetchNoteAP(r.id);
         if (note === 404) { db.prepare('DELETE FROM ap_timeline WHERE id = ?').run(r.id); healed++; continue; }
-        if (!note || typeof note !== 'object') continue;
+        if (!note || typeof note !== 'object') { failed++; continue; } // origin unreachable right now
         const html = HtmlSanitizerService.sanitize(note.content || '');
         const media = mediaFromNote(note);
@@ -2067,8 +2067,21 @@
           healed++;
         }
-      } catch { /* per-note best-effort */ }
-    }
-    try { db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)').run('selfheal_version', String(SELFHEAL_VERSION)); } catch { /* ignore */ }
-    if (rows.length) console.log(`[AP] self-heal v${SELFHEAL_VERSION}: ${healed}/${rows.length} timeline notes`);
+      } catch { failed++; /* per-note best-effort */ }
+    }
+    // Only mark this version DONE after a clean pass. Some origins are briefly
+    // offline exactly when we heal (phone-hosted instances!): skipping them and
+    // consuming the version would leave those rows stale forever. Instead retry
+    // on the next boots, giving up after a few attempts (permanently-dead
+    // origins answer 404/410 and are deleted above, so they don't loop).
+    const setSetting = (k, v) => { try { db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)').run(k, String(v)); } catch { /* ignore */ } };
+    let attempts = 0;
+    try { const a = db.prepare('SELECT value FROM app_settings WHERE key = ?').get('selfheal_attempts'); attempts = a ? (parseInt(a.value, 10) || 0) : 0; } catch { /* ignore */ }
+    if (failed === 0 || attempts >= 4) {
+      setSetting('selfheal_version', SELFHEAL_VERSION);
+      setSetting('selfheal_attempts', 0);
+    } else {
+      setSetting('selfheal_attempts', attempts + 1);
+    }
+    if (rows.length) console.log(`[AP] self-heal v${SELFHEAL_VERSION}: ${healed}/${rows.length} timeline notes${failed ? ` (${failed} unreachable — will retry next boot)` : ''}`);
   } catch { /* never block boot */ } finally { _selfHealing = false; }
 }
