Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 8ef8c3eac2476504a52ea0e74c93a6692330eecb)
+++ src/services/ActivityPubService.js	(revision d71ed0317f432dcfec0a7c029d67f8d6f429ff9f)
@@ -1054,4 +1054,36 @@
 }
 
+// One-time, best-effort migration of the old Cirkels (pull-protocol circle_links)
+// into ActivityPub follows with auto-boost. Runs once per instance at boot so a
+// site that updates past the old protocol keeps its cirkel without a manual step.
+async function resolveApActor(siteUrl) {
+  try {
+    const r = await fetch(siteUrl, { headers: { Accept: 'application/activity+json' }, redirect: 'manual' });
+    // A Klonkt site's root 302s to /ap/users/<slug> (Location may be relative).
+    if (r.status >= 300 && r.status < 400) { const loc = r.headers.get('location'); if (loc) return new URL(loc, siteUrl).href; }
+    if (r.ok) return siteUrl;
+  } catch { /* unreachable */ }
+  return null;
+}
+let _circlesMigrating = false;
+export async function autoMigrateCircles() {
+  if (_circlesMigrating) return; _circlesMigrating = true;
+  try {
+    let done; try { done = db.prepare('SELECT value FROM app_settings WHERE key = ?').get('circles_migrated_v1'); } catch { return; }
+    if (done && done.value === '1') return;
+    let links = [];
+    try { links = db.prepare("SELECT cl.remote_url AS url, s.id AS sid, s.slug AS slug FROM circle_links cl JOIN sites s ON s.id = cl.local_site_id WHERE cl.status = 'active'").all(); } catch { /* no legacy table */ }
+    let ok = 0;
+    for (const l of links) {
+      try {
+        const actor = await resolveApActor(l.url);
+        if (actor) { const r = await followActor({ id: l.sid, slug: l.slug }, actor, true); if (!(r && r.error)) ok++; }
+      } catch { /* best-effort per link */ }
+    }
+    try { db.prepare('INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)').run('circles_migrated_v1', '1'); } catch { /* ignore */ }
+    if (links.length) console.log(`[AP] circle migration: ${ok}/${links.length} legacy link(s) -> auto-boost`);
+  } catch { /* never block boot */ } finally { _circlesMigrating = false; }
+}
+
 // Follow a fediverse account by @handle (WebFinger → actor → signed Follow).
 export async function followActor(site, handle, autoBoost = false) {
@@ -1212,5 +1244,5 @@
   listOutbox, deliverOutboxDelete,
   webfingerResolve, followActor, resolveRemoteActor, unfollowActor, listFollowing, setAutoBoost, getTimeline, sendInteraction,
-  autoBoostCount, getCirkelPosts, getCirkelMembers,
+  autoBoostCount, getCirkelPosts, getCirkelMembers, autoMigrateCircles,
   getNotifications, listBlocks, isBlockedAny, blockTarget, unblock,
   deliverWithRetry, enqueueDelivery, processDeliveryQueue, startDeliveryWorker,
