thedotmack/claude-mem · warning

cloud sync unavailable; refusing an unreplicated delete

Error message

cloud sync unavailable; refusing an unreplicated delete

What it means

HTTP 503 from the same DataRoutes delete handler, on the opposite branch: cloud sync is NOT configured, but the row has an entry in sync_entity_heads — proof another device has already acknowledged it. Deleting it now would strand replicas that still reference the entity, so the worker refuses the unreplicated delete. This is a data-integrity guard, not a transient error.

Source

Thrown at src/services/worker/http/routes/DataRoutes.ts:373

    }

    const cloudSync = this.dbManager.getCloudSync();
    let entityRev: string | null = null;
    if (cloudSync?.isConfigured()) {
      if (!cloudSync.status().deviceId) {
        res.status(503).json({ error: 'cloud sync identity unavailable; refusing an unreplicated delete' });
        return;
      }
      entityRev = cloudSync.queueDelete(kind, originLocalId);
    } else {
      // A row with an acknowledged entity head must never be silently deleted
      // while its sync identity is unavailable: that would strand replicas.
      const acknowledged = store.db.prepare(`
        SELECT 1 AS found FROM sync_entity_heads
        WHERE kind = ? AND origin_local_id = ? LIMIT 1
      `).get(kind, originLocalId) as { found: number } | undefined;
      if (acknowledged) {
        res.status(503).json({ error: 'cloud sync unavailable; refusing an unreplicated delete' });
        return;
      }
      store.db.prepare(
        `DELETE FROM ${table} WHERE id = ? AND origin_device_id IS NULL`
      ).run(originLocalId);
    }

    res.json({ success: true, id: originLocalId, kind, entity_rev: entityRev });
  }

  private handleGetStats = this.wrapHandler((req: Request, res: Response): void => {
    const db = this.dbManager.getSessionStore().db;

    const packageRoot = getPackageRoot();
    const packageJsonPath = path.join(packageRoot, 'package.json');
    const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
    const version = packageJson.version;

View on GitHub (pinned to 8bc631a71a)

Solutions

  1. Reconfigure cloud sync so the delete replicates through queueDelete (the intended path)
  2. If going local-only permanently is deliberate, clear the stale sync_entity_heads rows for that entity or wipe the sync metadata — accepting other replicas keep their copy
  3. Verify you actually want the delete replicated: the guard exists to prevent silent divergence

Example fix

-- before: delete refused with 503 because entity head exists
DELETE /api/data/session/42

-- after: re-enable sync first, then delete replicates
-- (worker settings) cloudSync.configure({...})
DELETE /api/data/session/42  --> { success: true, entity_rev: "..." }
Defensive patterns

Strategy: retry

Validate before calling

async function deleteWillReplicate(base: string, kind: string, id: number): Promise<boolean> {
  // rough client-side check: sync configured OR no acknowledged heads
  const s = await (await fetch(`${base}/api/settings`)).json();
  return Boolean(s.cloudSync?.configured);
}

Type guard

function isUnreplicatedDeleteRefused(body: unknown, status: number): boolean {
  return status === 503 && typeof body === 'object' && body !== null &&
    String((body as { error?: string }).error).includes('unreplicated delete');
}

Try / catch

const res = await del(url);
if (res.status === 503 && (await res.json()).error.includes('cloud sync unavailable')) {
  // deliberate policy decision required: reconfigure sync, or purge sync_entity_heads
  await notifyUser('delete blocked to protect replicas');
}

Prevention

When it happens

Trigger: DELETE on a local-origin row that previously replicated (an entity head exists) after cloud sync was disabled, deconfigured, or its credentials expired; deleting data on a secondary machine whose sync setup was removed but whose database still carries acknowledged heads.

Common situations: User turns cloud sync off to 'go local' then tries to prune old sessions; sync token revoked server-side so isConfigured() is false while sync_entity_heads still holds rows; switching a machine between sync accounts.

Related errors


AI-assisted analysis of thedotmack/claude-mem@8bc631a71a (2026-08-20). Data as JSON: /api/errors/7962282ab6a60f38. Report an issue: GitHub.