diff --git a/daemons/attrd/Makefile.am b/daemons/attrd/Makefile.am
index f8d8bc91d6..3212efa289 100644
--- a/daemons/attrd/Makefile.am
+++ b/daemons/attrd/Makefile.am
@@ -1,49 +1,50 @@
 #
 # Copyright 2004-2023 the Pacemaker project contributors
 #
 # The version control history for this file may have further details.
 #
 # This source code is licensed under the GNU General Public License version 2
 # or later (GPLv2+) WITHOUT ANY WARRANTY.
 #
 
 include $(top_srcdir)/mk/common.mk
 
 halibdir	= $(CRM_DAEMON_DIR)
 
 halib_PROGRAMS	= pacemaker-attrd
 
 noinst_HEADERS  = pacemaker-attrd.h
 
 pacemaker_attrd_CFLAGS	= $(CFLAGS_HARDENED_EXE)
 pacemaker_attrd_LDFLAGS	= $(LDFLAGS_HARDENED_EXE)
 
 pacemaker_attrd_LDADD = $(top_builddir)/lib/cluster/libcrmcluster.la
 pacemaker_attrd_LDADD += $(top_builddir)/lib/cib/libcib.la
 pacemaker_attrd_LDADD += $(top_builddir)/lib/pengine/libpe_rules.la
 pacemaker_attrd_LDADD += $(top_builddir)/lib/lrmd/liblrmd.la
 pacemaker_attrd_LDADD += $(top_builddir)/lib/common/libcrmcommon.la
 pacemaker_attrd_LDADD += $(CLUSTERLIBS)
 
 pacemaker_attrd_SOURCES	= attrd_alerts.c 	\
 			  attrd_attributes.c 	\
 			  attrd_cib.c 		\
 			  attrd_corosync.c 	\
 			  attrd_elections.c 	\
 			  attrd_ipc.c 		\
 			  attrd_messages.c 	\
+			  attrd_nodes.c 	\
 			  attrd_sync.c 		\
 			  attrd_utils.c 	\
 			  pacemaker-attrd.c
 
 .PHONY: install-exec-hook
 install-exec-hook:
 if BUILD_LEGACY_LINKS
 	cd $(DESTDIR)$(CRM_DAEMON_DIR) && rm -f attrd && $(LN_S) pacemaker-attrd attrd
 endif
 
 .PHONY: uninstall-hook
 uninstall-hook:
 if BUILD_LEGACY_LINKS
 	cd $(DESTDIR)$(CRM_DAEMON_DIR) && rm -f attrd
 endif
diff --git a/daemons/attrd/attrd_attributes.c b/daemons/attrd/attrd_attributes.c
index 113f54f1e3..fe7e642f0e 100644
--- a/daemons/attrd/attrd_attributes.c
+++ b/daemons/attrd/attrd_attributes.c
@@ -1,283 +1,283 @@
 /*
  * Copyright 2013-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <errno.h>
 #include <stdbool.h>
 #include <stdlib.h>
 #include <glib.h>
 
 #include <crm/common/logging.h>
 #include <crm/common/results.h>
 #include <crm/common/strings_internal.h>
 #include <crm/common/xml.h>
 
 #include "pacemaker-attrd.h"
 
 static attribute_t *
 attrd_create_attribute(xmlNode *xml)
 {
     int is_private = 0;
     long long dampen = 0;
     const char *name = crm_element_value(xml, PCMK__XA_ATTR_NAME);
     const char *set_type = crm_element_value(xml, PCMK__XA_ATTR_SET_TYPE);
     const char *dampen_s = crm_element_value(xml, PCMK__XA_ATTR_DAMPENING);
     attribute_t *a = NULL;
 
     if (set_type == NULL) {
         set_type = PCMK_XE_INSTANCE_ATTRIBUTES;
     }
 
     /* Set type is meaningful only when writing to the CIB. Private
      * attributes are not written.
      */
     crm_element_value_int(xml, PCMK__XA_ATTR_IS_PRIVATE, &is_private);
     if (!is_private && !pcmk__str_any_of(set_type,
                                          PCMK_XE_INSTANCE_ATTRIBUTES,
                                          PCMK_XE_UTILIZATION, NULL)) {
         crm_warn("Ignoring attribute %s with invalid set type %s",
                  pcmk__s(name, "(unidentified)"), set_type);
         return NULL;
     }
 
     a = pcmk__assert_alloc(1, sizeof(attribute_t));
 
     a->id = pcmk__str_copy(name);
     a->set_type = pcmk__str_copy(set_type);
     a->set_id = crm_element_value_copy(xml, PCMK__XA_ATTR_SET);
     a->user = crm_element_value_copy(xml, PCMK__XA_ATTR_USER);
     a->values = pcmk__strikey_table(NULL, attrd_free_attribute_value);
 
     if (is_private) {
         attrd_set_attr_flags(a, attrd_attr_is_private);
     }
 
     if (dampen_s != NULL) {
         dampen = crm_get_msec(dampen_s);
     }
 
     if (dampen > 0) {
         a->timeout_ms = (int) QB_MIN(dampen, INT_MAX);
         a->timer = attrd_add_timer(a->id, a->timeout_ms, a);
     } else if (dampen < 0) {
         crm_warn("Ignoring invalid delay %s for attribute %s", dampen_s, a->id);
     }
 
     crm_trace("Created attribute %s with %s write delay and %s CIB user",
               a->id,
               ((dampen > 0)? pcmk__readable_interval(a->timeout_ms) : "no"),
               pcmk__s(a->user, "default"));
 
     g_hash_table_replace(attributes, a->id, a);
     return a;
 }
 
 static int
 attrd_update_dampening(attribute_t *a, xmlNode *xml, const char *attr)
 {
     const char *dvalue = crm_element_value(xml, PCMK__XA_ATTR_DAMPENING);
     long long dampen = 0;
 
     if (dvalue == NULL) {
         crm_warn("Could not update %s: peer did not specify value for delay",
                  attr);
         return EINVAL;
     }
 
     dampen = crm_get_msec(dvalue);
     if (dampen < 0) {
         crm_warn("Could not update %s: invalid delay value %dms (%s)",
                  attr, dampen, dvalue);
         return EINVAL;
     }
 
     if (a->timeout_ms != dampen) {
         mainloop_timer_del(a->timer);
         a->timeout_ms = (int) QB_MIN(dampen, INT_MAX);
         if (dampen > 0) {
             a->timer = attrd_add_timer(attr, a->timeout_ms, a);
             crm_info("Update attribute %s delay to %dms (%s)",
                      attr, dampen, dvalue);
         } else {
             a->timer = NULL;
             crm_info("Update attribute %s to remove delay", attr);
         }
 
         /* If dampening changed, do an immediate write-out,
          * otherwise repeated dampening changes would prevent write-outs
          */
         attrd_write_or_elect_attribute(a);
     }
 
     return pcmk_rc_ok;
 }
 
 GHashTable *attributes = NULL;
 
 /*!
  * \internal
  * \brief Create an XML representation of an attribute for use in peer messages
  *
  * \param[in,out] parent      Create attribute XML as child element of this
  * \param[in]     a           Attribute to represent
  * \param[in]     v           Attribute value to represent
  * \param[in]     force_write If true, value should be written even if unchanged
  *
  * \return XML representation of attribute
  */
 xmlNode *
 attrd_add_value_xml(xmlNode *parent, const attribute_t *a,
                     const attribute_value_t *v, bool force_write)
 {
     xmlNode *xml = pcmk__xe_create(parent, __func__);
 
     crm_xml_add(xml, PCMK__XA_ATTR_NAME, a->id);
     crm_xml_add(xml, PCMK__XA_ATTR_SET_TYPE, a->set_type);
     crm_xml_add(xml, PCMK__XA_ATTR_SET, a->set_id);
     crm_xml_add(xml, PCMK__XA_ATTR_USER, a->user);
     crm_xml_add(xml, PCMK__XA_ATTR_HOST, v->nodename);
 
     /* @COMPAT Prior to 2.1.10 and 3.0.1, the node's cluster ID was added
      * instead of its XML ID. For Corosync and Pacemaker Remote nodes, those are
      * the same, but if we ever support node XML IDs that differ from their
      * cluster IDs, we will have to drop support for rolling upgrades from
      * versions before those.
      */
-    crm_xml_add(xml, PCMK__XA_ATTR_HOST_ID, v->node_xml_id);
+    crm_xml_add(xml, PCMK__XA_ATTR_HOST_ID, attrd_get_node_xml_id(v->nodename));
 
     crm_xml_add(xml, PCMK__XA_ATTR_VALUE, v->current);
     crm_xml_add_int(xml, PCMK__XA_ATTR_DAMPENING, a->timeout_ms / 1000);
     crm_xml_add_int(xml, PCMK__XA_ATTR_IS_PRIVATE,
                     pcmk_is_set(a->flags, attrd_attr_is_private));
     crm_xml_add_int(xml, PCMK__XA_ATTR_IS_REMOTE,
                     pcmk_is_set(v->flags, attrd_value_remote));
     crm_xml_add_int(xml, PCMK__XA_ATTRD_IS_FORCE_WRITE, force_write);
 
     return xml;
 }
 
 void
 attrd_clear_value_seen(void)
 {
     GHashTableIter aIter;
     GHashTableIter vIter;
     attribute_t *a;
     attribute_value_t *v = NULL;
 
     g_hash_table_iter_init(&aIter, attributes);
     while (g_hash_table_iter_next(&aIter, NULL, (gpointer *) & a)) {
         g_hash_table_iter_init(&vIter, a->values);
         while (g_hash_table_iter_next(&vIter, NULL, (gpointer *) & v)) {
             attrd_clear_value_flags(v, attrd_value_from_peer);
         }
     }
 }
 
 attribute_t *
 attrd_populate_attribute(xmlNode *xml, const char *attr)
 {
     attribute_t *a = NULL;
     bool update_both = false;
 
     const char *op = crm_element_value(xml, PCMK_XA_TASK);
 
     // NULL because PCMK__ATTRD_CMD_SYNC_RESPONSE has no PCMK_XA_TASK
     update_both = pcmk__str_eq(op, PCMK__ATTRD_CMD_UPDATE_BOTH,
                                pcmk__str_null_matches);
 
     // Look up or create attribute entry
     a = g_hash_table_lookup(attributes, attr);
     if (a == NULL) {
         if (update_both || pcmk__str_eq(op, PCMK__ATTRD_CMD_UPDATE, pcmk__str_none)) {
             a = attrd_create_attribute(xml);
             if (a == NULL) {
                 return NULL;
             }
 
         } else {
             crm_warn("Could not update %s: attribute not found", attr);
             return NULL;
         }
     }
 
     // Update attribute dampening
     if (update_both || pcmk__str_eq(op, PCMK__ATTRD_CMD_UPDATE_DELAY, pcmk__str_none)) {
         int rc = attrd_update_dampening(a, xml, attr);
 
         if (rc != pcmk_rc_ok || !update_both) {
             return NULL;
         }
     }
 
     return a;
 }
 
 /*!
  * \internal
  * \brief Get the XML ID used to write out an attribute set
  *
  * \param[in] attr           Attribute to get set ID for
  * \param[in] node_state_id  XML ID of node state that attribute value is for
  *
  * \return Newly allocated string with XML ID to use for \p attr set
  */
 char *
 attrd_set_id(const attribute_t *attr, const char *node_state_id)
 {
     char *set_id = NULL;
 
     pcmk__assert((attr != NULL) && (node_state_id != NULL));
 
     if (attr->set_id == NULL) {
         /* @COMPAT This should really take the set type into account. Currently
          * we use the same XML ID for transient attributes and utilization
          * attributes. It doesn't cause problems because the status section is
          * not limited by the schema in any way, but it's still unfortunate.
          * For backward compatibility reasons, we can't change this.
          */
         set_id = crm_strdup_printf("%s-%s", PCMK_XE_STATUS, node_state_id);
     } else {
         /* @COMPAT When the user specifies a set ID for an attribute, it is the
          * same for every node. That is less than ideal, but again, the schema
          * doesn't enforce anything for the status section. We couldn't change
          * it without allowing the set ID to vary per value rather than per
          * attribute, which would break backward compatibility, pose design
          * challenges, and potentially cause problems in rolling upgrades.
          */
         set_id = pcmk__str_copy(attr->set_id);
     }
     crm_xml_sanitize_id(set_id);
     return set_id;
 }
 
 /*!
  * \internal
  * \brief Get the XML ID used to write out an attribute value
  *
  * \param[in] attr           Attribute to get value XML ID for
  * \param[in] node_state_id  UUID of node that attribute value is for
  *
  * \return Newly allocated string with XML ID of \p attr value
  */
 char *
 attrd_nvpair_id(const attribute_t *attr, const char *node_state_id)
 {
     char *nvpair_id = NULL;
 
     if (attr->set_id != NULL) {
         nvpair_id = crm_strdup_printf("%s-%s", attr->set_id, attr->id);
 
     } else {
         nvpair_id = crm_strdup_printf(PCMK_XE_STATUS "-%s-%s",
                                       node_state_id, attr->id);
     }
     crm_xml_sanitize_id(nvpair_id);
     return nvpair_id;
 }
diff --git a/daemons/attrd/attrd_cib.c b/daemons/attrd/attrd_cib.c
index 88993b502d..7f697e5048 100644
--- a/daemons/attrd/attrd_cib.c
+++ b/daemons/attrd/attrd_cib.c
@@ -1,703 +1,704 @@
 /*
  * Copyright 2013-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <errno.h>
 #include <stdbool.h>
 #include <stdlib.h>
 #include <glib.h>
 
 #include <crm/cib/internal.h>       // cib__*
 #include <crm/common/logging.h>
 #include <crm/common/results.h>
 #include <crm/common/strings_internal.h>
 #include <crm/common/xml.h>
 #include <crm/cluster/internal.h>   // pcmk__get_node()
 
 #include "pacemaker-attrd.h"
 
 static int last_cib_op_done = 0;
 
 static void write_attribute(attribute_t *a, bool ignore_delay);
 
 static void
 attrd_cib_destroy_cb(gpointer user_data)
 {
     cib_t *cib = user_data;
 
     cib->cmds->signoff(cib);
 
     if (attrd_shutting_down(false)) {
         crm_info("Disconnected from the CIB manager");
 
     } else {
         // @TODO This should trigger a reconnect, not a shutdown
         crm_crit("Lost connection to the CIB manager, shutting down");
         attrd_exit_status = CRM_EX_DISCONNECT;
         attrd_shutdown(0);
     }
 }
 
 static void
 attrd_cib_updated_cb(const char *event, xmlNode *msg)
 {
     const xmlNode *patchset = NULL;
     const char *client_name = NULL;
     bool status_changed = false;
 
     if (attrd_shutting_down(true)) {
         crm_debug("Ignoring CIB change during shutdown");
         return;
     }
 
     if (cib__get_notify_patchset(msg, &patchset) != pcmk_rc_ok) {
         return;
     }
 
     if (cib__element_in_patchset(patchset, PCMK_XE_ALERTS)) {
         mainloop_set_trigger(attrd_config_read);
     }
 
     status_changed = cib__element_in_patchset(patchset, PCMK_XE_STATUS);
 
     client_name = crm_element_value(msg, PCMK__XA_CIB_CLIENTNAME);
     if (!cib__client_triggers_refresh(client_name)) {
         /* This change came from a source that ensured the CIB is consistent
          * with our attributes table, so we don't need to write anything out.
          */
         return;
     }
 
     if (!attrd_election_won()) {
         // Don't write attributes if we're not the writer
         return;
     }
 
     if (status_changed || cib__element_in_patchset(patchset, PCMK_XE_NODES)) {
         /* An unsafe client modified the PCMK_XE_NODES or PCMK_XE_STATUS
          * section. Write transient attributes to ensure they're up-to-date in
          * the CIB.
          */
         if (client_name == NULL) {
             client_name = crm_element_value(msg, PCMK__XA_CIB_CLIENTID);
         }
         crm_notice("Updating all attributes after %s event triggered by %s",
                    event, pcmk__s(client_name, "(unidentified client)"));
 
         attrd_write_attributes(attrd_write_all);
     }
 }
 
 int
 attrd_cib_connect(int max_retry)
 {
     static int attempts = 0;
 
     int rc = -ENOTCONN;
 
     the_cib = cib_new();
     if (the_cib == NULL) {
         return -ENOTCONN;
     }
 
     do {
         if (attempts > 0) {
             sleep(attempts);
         }
         attempts++;
         crm_debug("Connection attempt %d to the CIB manager", attempts);
         rc = the_cib->cmds->signon(the_cib, PCMK__VALUE_ATTRD, cib_command);
 
     } while ((rc != pcmk_ok) && (attempts < max_retry));
 
     if (rc != pcmk_ok) {
         crm_err("Connection to the CIB manager failed: %s " CRM_XS " rc=%d",
                 pcmk_strerror(rc), rc);
         goto cleanup;
     }
 
     crm_debug("Connected to the CIB manager after %d attempts", attempts);
 
     rc = the_cib->cmds->set_connection_dnotify(the_cib, attrd_cib_destroy_cb);
     if (rc != pcmk_ok) {
         crm_err("Could not set disconnection callback");
         goto cleanup;
     }
 
     rc = the_cib->cmds->add_notify_callback(the_cib,
                                             PCMK__VALUE_CIB_DIFF_NOTIFY,
                                             attrd_cib_updated_cb);
     if (rc != pcmk_ok) {
         crm_err("Could not set CIB notification callback");
         goto cleanup;
     }
 
     return pcmk_ok;
 
 cleanup:
     cib__clean_up_connection(&the_cib);
     return -ENOTCONN;
 }
 
 void
 attrd_cib_disconnect(void)
 {
     CRM_CHECK(the_cib != NULL, return);
     the_cib->cmds->del_notify_callback(the_cib, PCMK__VALUE_CIB_DIFF_NOTIFY,
                                        attrd_cib_updated_cb);
     cib__clean_up_connection(&the_cib);
 }
 
 static void
 attrd_erase_cb(xmlNode *msg, int call_id, int rc, xmlNode *output,
                void *user_data)
 {
     const char *node = pcmk__s((const char *) user_data, "a node");
 
     if (rc == pcmk_ok) {
         crm_info("Cleared transient node attributes for %s from CIB", node);
     } else {
         crm_err("Unable to clear transient node attributes for %s from CIB: %s",
                 node, pcmk_strerror(rc));
     }
 }
 
 #define XPATH_TRANSIENT "//" PCMK__XE_NODE_STATE    \
                         "[@" PCMK_XA_UNAME "='%s']" \
                         "/" PCMK__XE_TRANSIENT_ATTRIBUTES
 
 /*!
  * \internal
  * \brief Wipe all transient node attributes for a node from the CIB
  *
  * \param[in] node  Node to clear attributes for
  */
 void
 attrd_cib_erase_transient_attrs(const char *node)
 {
     int call_id = 0;
     char *xpath = NULL;
 
     CRM_CHECK(node != NULL, return);
 
     xpath = crm_strdup_printf(XPATH_TRANSIENT, node);
 
     crm_debug("Clearing transient node attributes for %s from CIB using %s",
               node, xpath);
 
     call_id = the_cib->cmds->remove(the_cib, xpath, NULL, cib_xpath);
     free(xpath);
 
     the_cib->cmds->register_callback_full(the_cib, call_id, 120, FALSE,
                                           pcmk__str_copy(node),
                                           "attrd_erase_cb", attrd_erase_cb,
                                           free);
 }
 
 /*!
  * \internal
  * \brief Prepare the CIB after cluster is connected
  */
 void
 attrd_cib_init(void)
 {
     /* We have no attribute values in memory, so wipe the CIB to match. This is
      * normally done by the DC's controller when this node leaves the cluster, but
      * this handles the case where the node restarted so quickly that the
      * cluster layer didn't notice.
      *
      * \todo If pacemaker-attrd respawns after crashing (see PCMK_ENV_RESPAWNED),
      *       ideally we'd skip this and sync our attributes from the writer.
      *       However, currently we reject any values for us that the writer has, in
      *       attrd_peer_update().
      */
     attrd_cib_erase_transient_attrs(attrd_cluster->uname);
 
     // Set a trigger for reading the CIB (for the alerts section)
     attrd_config_read = mainloop_add_trigger(G_PRIORITY_HIGH, attrd_read_options, NULL);
 
     // Always read the CIB at start-up
     mainloop_set_trigger(attrd_config_read);
 }
 
 static gboolean
 attribute_timer_cb(gpointer data)
 {
     attribute_t *a = data;
     crm_trace("Dampen interval expired for %s", a->id);
     attrd_write_or_elect_attribute(a);
     return FALSE;
 }
 
 static void
 attrd_cib_callback(xmlNode *msg, int call_id, int rc, xmlNode *output, void *user_data)
 {
     int level = LOG_ERR;
     GHashTableIter iter;
     const char *peer = NULL;
     attribute_value_t *v = NULL;
 
     char *name = user_data;
     attribute_t *a = g_hash_table_lookup(attributes, name);
 
     if(a == NULL) {
         crm_info("Attribute %s no longer exists", name);
         return;
     }
 
     a->update = 0;
     if (rc == pcmk_ok && call_id < 0) {
         rc = call_id;
     }
 
     switch (rc) {
         case pcmk_ok:
             level = LOG_INFO;
             last_cib_op_done = call_id;
             if (a->timer && !a->timeout_ms) {
                 // Remove temporary dampening for failed writes
                 mainloop_timer_del(a->timer);
                 a->timer = NULL;
             }
             break;
 
         case -pcmk_err_diff_failed:    /* When an attr changes while the CIB is syncing */
         case -ETIME:           /* When an attr changes while there is a DC election */
         case -ENXIO:           /* When an attr changes while the CIB is syncing a
                                 *   newer config from a node that just came up
                                 */
             level = LOG_WARNING;
             break;
     }
 
     do_crm_log(level, "CIB update %d result for %s: %s " CRM_XS " rc=%d",
                call_id, a->id, pcmk_strerror(rc), rc);
 
     g_hash_table_iter_init(&iter, a->values);
     while (g_hash_table_iter_next(&iter, (gpointer *) & peer, (gpointer *) & v)) {
         if (rc == pcmk_ok) {
             crm_info("* Wrote %s[%s]=%s",
                      a->id, peer, pcmk__s(v->requested, "(unset)"));
             pcmk__str_update(&(v->requested), NULL);
         } else {
             do_crm_log(level, "* Could not write %s[%s]=%s",
                        a->id, peer, pcmk__s(v->requested, "(unset)"));
             /* Reattempt write below if we are still the writer */
             attrd_set_attr_flags(a, attrd_attr_changed);
         }
     }
 
     if (pcmk_is_set(a->flags, attrd_attr_changed) && attrd_election_won()) {
         if (rc == pcmk_ok) {
             /* We deferred a write of a new update because this update was in
              * progress. Write out the new value without additional delay.
              */
             crm_debug("Pending update for %s can be written now", a->id);
             write_attribute(a, false);
 
         /* We're re-attempting a write because the original failed; delay
          * the next attempt so we don't potentially flood the CIB manager
          * and logs with a zillion attempts per second.
          *
          * @TODO We could elect a new writer instead. However, we'd have to
          * somehow downgrade our vote, and we'd still need something like this
          * if all peers similarly fail to write this attribute (which may
          * indicate a corrupted attribute entry rather than a CIB issue).
          */
         } else if (a->timer) {
             // Attribute has a dampening value, so use that as delay
             if (!mainloop_timer_running(a->timer)) {
                 crm_trace("Delayed re-attempted write for %s by %s",
                           name, pcmk__readable_interval(a->timeout_ms));
                 mainloop_timer_start(a->timer);
             }
         } else {
             /* Set a temporary dampening of 2 seconds (timer will continue
              * to exist until the attribute's dampening gets set or the
              * write succeeds).
              */
             a->timer = attrd_add_timer(a->id, 2000, a);
             mainloop_timer_start(a->timer);
         }
     }
 }
 
 /*!
  * \internal
  * \brief Add a set-attribute update request to the current CIB transaction
  *
  * \param[in] attr     Attribute to update
  * \param[in] attr_id  ID of attribute to update
  * \param[in] node_id  ID of node for which to update attribute value
  * \param[in] set_id   ID of attribute set
  * \param[in] value    New value for attribute
  *
  * \return Standard Pacemaker return code
  */
 static int
 add_set_attr_update(const attribute_t *attr, const char *attr_id,
                     const char *node_id, const char *set_id, const char *value)
 {
     xmlNode *update = pcmk__xe_create(NULL, PCMK__XE_NODE_STATE);
     xmlNode *child = update;
     int rc = ENOMEM;
 
     crm_xml_add(child, PCMK_XA_ID, node_id);
 
     child = pcmk__xe_create(child, PCMK__XE_TRANSIENT_ATTRIBUTES);
     crm_xml_add(child, PCMK_XA_ID, node_id);
 
     child = pcmk__xe_create(child, attr->set_type);
     crm_xml_add(child, PCMK_XA_ID, set_id);
 
     child = pcmk__xe_create(child, PCMK_XE_NVPAIR);
     crm_xml_add(child, PCMK_XA_ID, attr_id);
     crm_xml_add(child, PCMK_XA_NAME, attr->id);
     crm_xml_add(child, PCMK_XA_VALUE, value);
 
     rc = the_cib->cmds->modify(the_cib, PCMK_XE_STATUS, update,
                                cib_can_create|cib_transaction);
     rc = pcmk_legacy2rc(rc);
 
     free_xml(update);
     return rc;
 }
 
 /*!
  * \internal
  * \brief Add an unset-attribute update request to the current CIB transaction
  *
  * \param[in] attr     Attribute to update
  * \param[in] attr_id  ID of attribute to update
  * \param[in] node_id  ID of node for which to update attribute value
  * \param[in] set_id   ID of attribute set
  *
  * \return Standard Pacemaker return code
  */
 static int
 add_unset_attr_update(const attribute_t *attr, const char *attr_id,
                       const char *node_id, const char *set_id)
 {
     char *xpath = crm_strdup_printf("/" PCMK_XE_CIB
                                     "/" PCMK_XE_STATUS
                                     "/" PCMK__XE_NODE_STATE
                                         "[@" PCMK_XA_ID "='%s']"
                                     "/" PCMK__XE_TRANSIENT_ATTRIBUTES
                                         "[@" PCMK_XA_ID "='%s']"
                                     "/%s[@" PCMK_XA_ID "='%s']"
                                     "/" PCMK_XE_NVPAIR
                                         "[@" PCMK_XA_ID "='%s' "
                                          "and @" PCMK_XA_NAME "='%s']",
                                     node_id, node_id, attr->set_type, set_id,
                                     attr_id, attr->id);
 
     int rc = the_cib->cmds->remove(the_cib, xpath, NULL,
                                    cib_xpath|cib_transaction);
 
     free(xpath);
     return pcmk_legacy2rc(rc);
 }
 
 /*!
  * \internal
  * \brief Add an attribute update request to the current CIB transaction
  *
  * \param[in] attr      Attribute to update
  * \param[in] value     New value for attribute
  * \param[in] node_id   ID of node for which to update attribute value
  *
  * \return Standard Pacemaker return code
  */
 static int
 add_attr_update(const attribute_t *attr, const char *value, const char *node_id)
 {
     char *set_id = attrd_set_id(attr, node_id);
     char *nvpair_id = attrd_nvpair_id(attr, node_id);
     int rc = pcmk_rc_ok;
 
     if (value == NULL) {
         rc = add_unset_attr_update(attr, nvpair_id, node_id, set_id);
     } else {
         rc = add_set_attr_update(attr, nvpair_id, node_id, set_id, value);
     }
     free(set_id);
     free(nvpair_id);
     return rc;
 }
 
 static void
 send_alert_attributes_value(attribute_t *a, GHashTable *t)
 {
     int rc = 0;
     attribute_value_t *at = NULL;
     GHashTableIter vIter;
 
     g_hash_table_iter_init(&vIter, t);
 
     while (g_hash_table_iter_next(&vIter, NULL, (gpointer *) & at)) {
-        rc = attrd_send_attribute_alert(at->nodename, at->node_xml_id,
+        const char *node_xml_id = attrd_get_node_xml_id(at->nodename);
+
+        rc = attrd_send_attribute_alert(at->nodename, node_xml_id,
                                         a->id, at->current);
         crm_trace("Sent alerts for %s[%s]=%s with node XML ID %s "
                   "(%s agents failed)",
-                  a->id, at->nodename, at->current, at->node_xml_id,
+                  a->id, at->nodename, at->current,
+                  pcmk__s(node_xml_id, "unknown"),
                   ((rc == 0)? "no" : ((rc == -1)? "some" : "all")));
     }
 }
 
 static void
 set_alert_attribute_value(GHashTable *t, attribute_value_t *v)
 {
     attribute_value_t *a_v = pcmk__assert_alloc(1, sizeof(attribute_value_t));
 
-    a_v->node_xml_id = pcmk__str_copy(v->node_xml_id);
     a_v->nodename = pcmk__str_copy(v->nodename);
     a_v->current = pcmk__str_copy(v->current);
 
     g_hash_table_replace(t, a_v->nodename, a_v);
 }
 
 mainloop_timer_t *
 attrd_add_timer(const char *id, int timeout_ms, attribute_t *attr)
 {
    return mainloop_timer_add(id, timeout_ms, FALSE, attribute_timer_cb, attr);
 }
 
 /*!
  * \internal
  * \brief Write an attribute's values to the CIB if appropriate
  *
  * \param[in,out] a             Attribute to write
  * \param[in]     ignore_delay  If true, write attribute now regardless of any
  *                              configured delay
  */
 static void
 write_attribute(attribute_t *a, bool ignore_delay)
 {
     int private_updates = 0, cib_updates = 0;
     attribute_value_t *v = NULL;
     GHashTableIter iter;
     GHashTable *alert_attribute_value = NULL;
     int rc = pcmk_ok;
     bool should_write = true;
 
     if (a == NULL) {
         return;
     }
 
     // Private attributes (or any in standalone mode) are not written to the CIB
     if (stand_alone || pcmk_is_set(a->flags, attrd_attr_is_private)) {
         should_write = false;
     }
 
     /* If this attribute will be written to the CIB ... */
     if (should_write) {
         /* Defer the write if now's not a good time */
         if (a->update && (a->update < last_cib_op_done)) {
             crm_info("Write out of '%s' continuing: update %d considered lost",
                      a->id, a->update);
             a->update = 0; // Don't log this message again
 
         } else if (a->update) {
             crm_info("Write out of '%s' delayed: update %d in progress",
                      a->id, a->update);
             goto done;
 
         } else if (mainloop_timer_running(a->timer)) {
             if (ignore_delay) {
                 mainloop_timer_stop(a->timer);
                 crm_debug("Overriding '%s' write delay", a->id);
             } else {
                 crm_info("Delaying write of '%s'", a->id);
                 goto done;
             }
         }
 
         // Initiate a transaction for all the peer value updates
         CRM_CHECK(the_cib != NULL, goto done);
         the_cib->cmds->set_user(the_cib, a->user);
         rc = the_cib->cmds->init_transaction(the_cib);
         if (rc != pcmk_ok) {
             crm_err("Failed to write %s (set %s): Could not initiate "
                     "CIB transaction",
                     a->id, pcmk__s(a->set_id, "unspecified"));
             goto done;
         }
     }
 
     /* The changed and force-write flags apply only to the next write,
      * which this is, so clear them now. Also clear the "node unknown" flag
      * because we will check whether it is known below and reset if appopriate.
      */
     attrd_clear_attr_flags(a, attrd_attr_changed
                               |attrd_attr_force_write
                               |attrd_attr_node_unknown);
 
     /* Make the table for the attribute trap */
     alert_attribute_value = pcmk__strikey_table(NULL,
                                                 attrd_free_attribute_value);
 
     /* Iterate over each peer value of this attribute */
     g_hash_table_iter_init(&iter, a->values);
     while (g_hash_table_iter_next(&iter, NULL, (gpointer *) &v)) {
         const char *node_xml_id = NULL;
+        const char *prev_xml_id = NULL;
 
         if (!should_write) {
             private_updates++;
             continue;
         }
 
         /* We need the node's CIB XML ID to write out its attributes, so look
          * for it now. Check the node caches first, even if the ID was
          * previously known (in case it changed), but use any previous value as
          * a fallback.
          */
 
+        prev_xml_id = attrd_get_node_xml_id(v->nodename);
+
         if (pcmk_is_set(v->flags, attrd_value_remote)) {
             // A Pacemaker Remote node's XML ID is the same as its name
             node_xml_id = v->nodename;
 
         } else {
             // This creates a cluster node cache entry if none exists
-            crm_node_t *peer = pcmk__get_node(0, v->nodename, v->node_xml_id,
+            crm_node_t *peer = pcmk__get_node(0, v->nodename, prev_xml_id,
                                               pcmk__node_search_any);
 
             node_xml_id = pcmk__cluster_get_xml_id(peer);
             if (node_xml_id == NULL) {
-                node_xml_id = v->node_xml_id;
+                node_xml_id = prev_xml_id;
             }
         }
 
         // Defer write if this is a cluster node that's never been seen
         if (node_xml_id == NULL) {
             attrd_set_attr_flags(a, attrd_attr_node_unknown);
             crm_notice("Cannot write %s[%s]='%s' to CIB because node's XML ID "
                        "is unknown (will retry if learned)",
                        a->id, v->nodename, v->current);
             continue;
         }
 
-        /* Remember the XML ID and let peers know it (in case one of them
-         * becomes the writer later)
-         */
-        if (!pcmk__str_eq(v->node_xml_id, node_xml_id, pcmk__str_none)) {
+        if (!pcmk__str_eq(prev_xml_id, node_xml_id, pcmk__str_none)) {
             crm_trace("Setting %s[%s] node XML ID to %s (was %s)",
                       a->id, v->nodename, node_xml_id,
-                      pcmk__s(v->node_xml_id, "unknown"));
-            pcmk__str_update(&(v->node_xml_id), node_xml_id);
-            attrd_broadcast_value(a, v);
+                      pcmk__s(prev_xml_id, "unknown"));
+            attrd_set_node_xml_id(v->nodename, node_xml_id);
         }
 
         // Update this value as part of the CIB transaction we're building
         rc = add_attr_update(a, v->current, node_xml_id);
         if (rc != pcmk_rc_ok) {
             crm_err("Couldn't add %s[%s]='%s' to CIB transaction: %s "
                     CRM_XS " node XML ID %s",
                     a->id, v->nodename, v->current, pcmk_rc_str(rc),
                     node_xml_id);
             continue;
         }
 
         crm_debug("Added %s[%s]=%s to CIB transaction (node XML ID %s)",
                   a->id, v->nodename, pcmk__s(v->current, "(unset)"),
                   node_xml_id);
         cib_updates++;
 
         /* Preservation of the attribute to transmit alert */
         set_alert_attribute_value(alert_attribute_value, v);
 
         // Save this value so we can log it when write completes
         pcmk__str_update(&(v->requested), v->current);
     }
 
     if (private_updates) {
         crm_info("Processed %d private change%s for %s (set %s)",
                  private_updates, pcmk__plural_s(private_updates),
                  a->id, pcmk__s(a->set_id, "unspecified"));
     }
     if (cib_updates > 0) {
         char *id = pcmk__str_copy(a->id);
 
         // Commit transaction
         a->update = the_cib->cmds->end_transaction(the_cib, true, cib_none);
 
         crm_info("Sent CIB request %d with %d change%s for %s (set %s)",
                  a->update, cib_updates, pcmk__plural_s(cib_updates),
                  a->id, pcmk__s(a->set_id, "unspecified"));
 
         if (the_cib->cmds->register_callback_full(the_cib, a->update,
                                                   CIB_OP_TIMEOUT_S, FALSE, id,
                                                   "attrd_cib_callback",
                                                   attrd_cib_callback, free)) {
             // Transmit alert of the attribute
             send_alert_attributes_value(a, alert_attribute_value);
         }
     }
 
 done:
     // Discard transaction (if any)
     if (the_cib != NULL) {
         the_cib->cmds->end_transaction(the_cib, false, cib_none);
         the_cib->cmds->set_user(the_cib, NULL);
     }
 
     if (alert_attribute_value != NULL) {
         g_hash_table_destroy(alert_attribute_value);
     }
 }
 
 /*!
  * \internal
  * \brief Write out attributes
  *
  * \param[in] options  Group of enum attrd_write_options
  */
 void
 attrd_write_attributes(uint32_t options)
 {
     GHashTableIter iter;
     attribute_t *a = NULL;
 
     crm_debug("Writing out %s attributes",
               pcmk_is_set(options, attrd_write_all)? "all" : "changed");
     g_hash_table_iter_init(&iter, attributes);
     while (g_hash_table_iter_next(&iter, NULL, (gpointer *) & a)) {
         if (!pcmk_is_set(options, attrd_write_all)
             && pcmk_is_set(a->flags, attrd_attr_node_unknown)) {
             // Try writing this attribute again, in case peer ID was learned
             attrd_set_attr_flags(a, attrd_attr_changed);
         } else if (pcmk_is_set(a->flags, attrd_attr_force_write)) {
             /* If the force_write flag is set, write the attribute. */
             attrd_set_attr_flags(a, attrd_attr_changed);
         }
 
         if (pcmk_is_set(options, attrd_write_all) ||
             pcmk_is_set(a->flags, attrd_attr_changed)) {
             bool ignore_delay = pcmk_is_set(options, attrd_write_no_delay);
 
             if (pcmk_is_set(a->flags, attrd_attr_force_write)) {
                 // Always ignore delay when forced write flag is set
                 ignore_delay = true;
             }
             write_attribute(a, ignore_delay);
         } else {
             crm_trace("Skipping unchanged attribute %s", a->id);
         }
     }
 }
 
 void
 attrd_write_or_elect_attribute(attribute_t *a)
 {
     if (attrd_election_won()) {
         write_attribute(a, false);
     } else {
         attrd_start_election_if_needed();
     }
 }
diff --git a/daemons/attrd/attrd_corosync.c b/daemons/attrd/attrd_corosync.c
index a7f75a2b9a..488c5db87d 100644
--- a/daemons/attrd/attrd_corosync.c
+++ b/daemons/attrd/attrd_corosync.c
@@ -1,611 +1,614 @@
 /*
  * Copyright 2013-2025 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <errno.h>
 #include <stdbool.h>
 #include <stdint.h>
 #include <stdlib.h>
 
 #include <crm/cluster.h>
 #include <crm/cluster/internal.h>
 #include <crm/common/logging.h>
 #include <crm/common/results.h>
 #include <crm/common/strings_internal.h>
 #include <crm/common/xml.h>
 
 #include "pacemaker-attrd.h"
 
 static xmlNode *
 attrd_confirmation(int callid)
 {
     xmlNode *node = pcmk__xe_create(NULL, __func__);
 
     crm_xml_add(node, PCMK__XA_T, PCMK__VALUE_ATTRD);
     crm_xml_add(node, PCMK__XA_SRC, pcmk__cluster_local_node_name());
     crm_xml_add(node, PCMK_XA_TASK, PCMK__ATTRD_CMD_CONFIRM);
     crm_xml_add_int(node, PCMK__XA_CALL_ID, callid);
 
     return node;
 }
 
 static void
 attrd_peer_message(crm_node_t *peer, xmlNode *xml)
 {
     const char *election_op = crm_element_value(xml, PCMK__XA_CRM_TASK);
 
     if (election_op) {
         attrd_handle_election_op(peer, xml);
         return;
     }
 
     if (attrd_shutting_down(false)) {
         /* If we're shutting down, we want to continue responding to election
          * ops as long as we're a cluster member (because our vote may be
          * needed). Ignore all other messages.
          */
         return;
 
     } else {
         pcmk__request_t request = {
             .ipc_client     = NULL,
             .ipc_id         = 0,
             .ipc_flags      = 0,
             .peer           = peer->uname,
             .xml            = xml,
             .call_options   = 0,
             .result         = PCMK__UNKNOWN_RESULT,
         };
 
         request.op = crm_element_value_copy(request.xml, PCMK_XA_TASK);
         CRM_CHECK(request.op != NULL, return);
 
         attrd_handle_request(&request);
 
         /* Having finished handling the request, check to see if the originating
          * peer requested confirmation.  If so, send that confirmation back now.
          */
         if (pcmk__xe_attr_is_true(xml, PCMK__XA_CONFIRM) &&
             !pcmk__str_eq(request.op, PCMK__ATTRD_CMD_CONFIRM, pcmk__str_none)) {
             int callid = 0;
             xmlNode *reply = NULL;
 
             /* Add the confirmation ID for the message we are confirming to the
              * response so the originating peer knows what they're a confirmation
              * for.
              */
             crm_element_value_int(xml, PCMK__XA_CALL_ID, &callid);
             reply = attrd_confirmation(callid);
 
             /* And then send the confirmation back to the originating peer.  This
              * ends up right back in this same function (attrd_peer_message) on the
              * peer where it will have to do something with a PCMK__XA_CONFIRM type
              * message.
              */
             crm_debug("Sending %s a confirmation", peer->uname);
             attrd_send_message(peer, reply, false);
             free_xml(reply);
         }
 
         pcmk__reset_request(&request);
     }
 }
 
 static void
 attrd_cpg_dispatch(cpg_handle_t handle,
                  const struct cpg_name *groupName,
                  uint32_t nodeid, uint32_t pid, void *msg, size_t msg_len)
 {
     uint32_t kind = 0;
     xmlNode *xml = NULL;
     const char *from = NULL;
     char *data = pcmk__cpg_message_data(handle, nodeid, pid, msg, &kind, &from);
 
     if(data == NULL) {
         return;
     }
 
     if (kind == crm_class_cluster) {
         xml = pcmk__xml_parse(data);
     }
 
     if (xml == NULL) {
         crm_err("Bad message of class %d received from %s[%u]: '%.120s'", kind, from, nodeid, data);
     } else {
         attrd_peer_message(pcmk__get_node(nodeid, from, NULL,
                                           pcmk__node_search_cluster_member),
                            xml);
     }
 
     free_xml(xml);
     free(data);
 }
 
 static void
 attrd_cpg_destroy(gpointer unused)
 {
     if (attrd_shutting_down(false)) {
         crm_info("Disconnected from Corosync process group");
 
     } else {
         crm_crit("Lost connection to Corosync process group, shutting down");
         attrd_exit_status = CRM_EX_DISCONNECT;
         attrd_shutdown(0);
     }
 }
 
 /*!
  * \internal
  * \brief Broadcast an update for a single attribute value
  *
  * \param[in] a  Attribute to broadcast
  * \param[in] v  Attribute value to broadcast
  */
 void
 attrd_broadcast_value(const attribute_t *a, const attribute_value_t *v)
 {
     xmlNode *op = pcmk__xe_create(NULL, PCMK_XE_OP);
 
     crm_xml_add(op, PCMK_XA_TASK, PCMK__ATTRD_CMD_UPDATE);
     attrd_add_value_xml(op, a, v, false);
     attrd_send_message(NULL, op, false);
     free_xml(op);
 }
 
 #define state_text(state) pcmk__s((state), "in unknown state")
 
 static void
 attrd_peer_change_cb(enum crm_status_type kind, crm_node_t *peer, const void *data)
 {
     bool gone = false;
     bool is_remote = pcmk_is_set(peer->flags, crm_remote_node);
 
     switch (kind) {
         case crm_status_uname:
             crm_debug("%s node %s is now %s",
                       (is_remote? "Remote" : "Cluster"),
                       peer->uname, state_text(peer->state));
             break;
 
         case crm_status_processes:
             if (!pcmk_is_set(peer->processes, crm_get_cluster_proc())) {
                 gone = true;
             }
             crm_debug("Node %s is %s a peer",
                       peer->uname, (gone? "no longer" : "now"));
             break;
 
         case crm_status_nstate:
             crm_debug("%s node %s is now %s (was %s)",
                       (is_remote? "Remote" : "Cluster"),
                       peer->uname, state_text(peer->state), state_text(data));
             if (pcmk__str_eq(peer->state, CRM_NODE_MEMBER, pcmk__str_casei)) {
                 /* If we're the writer, send new peers a list of all attributes
                  * (unless it's a remote node, which doesn't run its own attrd)
                  */
                 if (attrd_election_won()
                     && !pcmk_is_set(peer->flags, crm_remote_node)) {
                     attrd_peer_sync(peer);
                 }
             } else {
                 // Remove all attribute values associated with lost nodes
                 attrd_peer_remove(peer->uname, false, "loss");
                 gone = true;
             }
             break;
     }
 
     // Remove votes from cluster nodes that leave, in case election in progress
     if (gone && !is_remote) {
         attrd_remove_voter(peer);
         attrd_remove_peer_protocol_ver(peer->uname);
         attrd_do_not_expect_from_peer(peer->uname);
     }
 }
 
 #define readable_value(rv_v) pcmk__s((rv_v)->current, "(unset)")
 
 #define readable_peer(p)    \
     (((p) == NULL)? "all peers" : pcmk__s((p)->uname, "unknown peer"))
 
 static void
 update_attr_on_host(attribute_t *a, const crm_node_t *peer, const xmlNode *xml,
                     const char *attr, const char *value, const char *host,
                     bool filter)
 {
     int is_remote = 0;
     bool changed = false;
     attribute_value_t *v = NULL;
+    const char *prev_xml_id = NULL;
     const char *node_xml_id = crm_element_value(xml, PCMK__XA_ATTR_HOST_ID);
 
     // Create entry for value if not already existing
     v = g_hash_table_lookup(a->values, host);
     if (v == NULL) {
         v = pcmk__assert_alloc(1, sizeof(attribute_value_t));
 
         v->nodename = pcmk__str_copy(host);
         g_hash_table_replace(a->values, v->nodename, v);
     }
 
     /* If update doesn't contain the node XML ID, fall back to any previously
      * known value (for logging)
      */
+    prev_xml_id = attrd_get_node_xml_id(v->nodename);
     if (node_xml_id == NULL) {
-        node_xml_id = v->node_xml_id;
+        node_xml_id = prev_xml_id;
     }
 
     // If value is for a Pacemaker Remote node, remember that
     crm_element_value_int(xml, PCMK__XA_ATTR_IS_REMOTE, &is_remote);
     if (is_remote) {
         attrd_set_value_flags(v, attrd_value_remote);
         pcmk__assert(pcmk__cluster_lookup_remote_node(host) != NULL);
     }
 
     // Check whether the value changed
     changed = !pcmk__str_eq(v->current, value, pcmk__str_casei);
 
     if (changed && filter && pcmk__str_eq(host, attrd_cluster->uname,
                                           pcmk__str_casei)) {
         /* Broadcast the local value for an attribute that differs from the
          * value provided in a peer's attribute synchronization response. This
          * ensures a node's values for itself take precedence and all peers are
          * kept in sync.
          */
         v = g_hash_table_lookup(a->values, attrd_cluster->uname);
         crm_notice("%s[%s]: local value '%s' takes priority over '%s' from %s",
                    attr, host, readable_value(v), value, peer->uname);
         attrd_broadcast_value(a, v);
 
     } else if (changed) {
         crm_notice("Setting %s[%s]%s%s: %s -> %s "
                    CRM_XS " from %s with %s write delay and node XML ID %s",
                    attr, host, a->set_type ? " in " : "",
                    pcmk__s(a->set_type, ""), readable_value(v),
                    pcmk__s(value, "(unset)"), peer->uname,
                    (a->timeout_ms == 0)? "no" : pcmk__readable_interval(a->timeout_ms),
                    pcmk__s(node_xml_id, "unknown"));
         pcmk__str_update(&v->current, value);
         attrd_set_attr_flags(a, attrd_attr_changed);
 
         if (pcmk__str_eq(host, attrd_cluster->uname, pcmk__str_casei)
             && pcmk__str_eq(attr, PCMK__NODE_ATTR_SHUTDOWN, pcmk__str_none)) {
 
             if (!pcmk__str_eq(value, "0", pcmk__str_null_matches)) {
                 attrd_set_requesting_shutdown();
 
             } else {
                 attrd_clear_requesting_shutdown();
             }
         }
 
         // Write out new value or start dampening timer
         if (a->timeout_ms && a->timer) {
             crm_trace("Delaying write of %s %s for dampening",
                       attr, pcmk__readable_interval(a->timeout_ms));
             mainloop_timer_start(a->timer);
         } else {
             attrd_write_or_elect_attribute(a);
         }
 
     } else {
         int is_force_write = 0;
 
         crm_element_value_int(xml, PCMK__XA_ATTRD_IS_FORCE_WRITE,
                               &is_force_write);
 
         if (is_force_write == 1 && a->timeout_ms && a->timer) {
             /* Save forced writing and set change flag. */
             /* The actual attribute is written by Writer after election. */
             crm_trace("%s[%s] from %s is unchanged (%s), forcing write",
                       attr, host, peer->uname, pcmk__s(value, "unset"));
             attrd_set_attr_flags(a, attrd_attr_force_write);
         } else {
             crm_trace("%s[%s] from %s is unchanged (%s)",
                       attr, host, peer->uname, pcmk__s(value, "unset"));
         }
     }
 
     // This allows us to later detect local values that peer doesn't know about
     attrd_set_value_flags(v, attrd_value_from_peer);
 
     // Remember node's XML ID if we're just learning it
     if ((node_xml_id != NULL)
-        && !pcmk__str_eq(node_xml_id, v->node_xml_id, pcmk__str_none)) {
+        && !pcmk__str_eq(node_xml_id, prev_xml_id, pcmk__str_none)) {
         crm_trace("Learned %s[%s] node XML ID is %s (was %s)",
                   a->id, v->nodename, node_xml_id,
-                  pcmk__s(v->node_xml_id, "unknown"));
-        pcmk__str_update(&(v->node_xml_id), node_xml_id);
+                  pcmk__s(prev_xml_id, "unknown"));
+        attrd_set_node_xml_id(v->nodename, node_xml_id);
         if (attrd_election_won()) {
             // In case we couldn't write a value missing the XML ID before
             attrd_write_attributes(attrd_write_changed);
         }
     }
 }
 
 static void
 attrd_peer_update_one(const crm_node_t *peer, xmlNode *xml, bool filter)
 {
     attribute_t *a = NULL;
     const char *attr = crm_element_value(xml, PCMK__XA_ATTR_NAME);
     const char *value = crm_element_value(xml, PCMK__XA_ATTR_VALUE);
     const char *host = crm_element_value(xml, PCMK__XA_ATTR_HOST);
 
     if (attr == NULL) {
         crm_warn("Could not update attribute: peer did not specify name");
         return;
     }
 
     a = attrd_populate_attribute(xml, attr);
     if (a == NULL) {
         return;
     }
 
     if (host == NULL) {
         // If no host was specified, update all hosts
         GHashTableIter vIter;
 
         crm_debug("Setting %s for all hosts to %s", attr, value);
         pcmk__xe_remove_attr(xml, PCMK__XA_ATTR_HOST_ID);
         g_hash_table_iter_init(&vIter, a->values);
 
         while (g_hash_table_iter_next(&vIter, (gpointer *) & host, NULL)) {
             update_attr_on_host(a, peer, xml, attr, value, host, filter);
         }
 
     } else {
         // Update attribute value for the given host
         update_attr_on_host(a, peer, xml, attr, value, host, filter);
     }
 
     /* If this is a message from some attrd instance broadcasting its protocol
      * version, check to see if it's a new minimum version.
      */
     if (pcmk__str_eq(attr, CRM_ATTR_PROTOCOL, pcmk__str_none)) {
         attrd_update_minimum_protocol_ver(peer->uname, value);
     }
 }
 
 static void
 broadcast_unseen_local_values(void)
 {
     GHashTableIter aIter;
     GHashTableIter vIter;
     attribute_t *a = NULL;
     attribute_value_t *v = NULL;
     xmlNode *sync = NULL;
 
     g_hash_table_iter_init(&aIter, attributes);
     while (g_hash_table_iter_next(&aIter, NULL, (gpointer *) & a)) {
 
         g_hash_table_iter_init(&vIter, a->values);
         while (g_hash_table_iter_next(&vIter, NULL, (gpointer *) & v)) {
 
             if (!pcmk_is_set(v->flags, attrd_value_from_peer)
                 && pcmk__str_eq(v->nodename, attrd_cluster->uname,
                                 pcmk__str_casei)) {
                 crm_trace("* %s[%s]='%s' is local-only",
                           a->id, v->nodename, readable_value(v));
                 if (sync == NULL) {
                     sync = pcmk__xe_create(NULL, __func__);
                     crm_xml_add(sync, PCMK_XA_TASK, PCMK__ATTRD_CMD_SYNC_RESPONSE);
                 }
                 attrd_add_value_xml(sync, a, v, a->timeout_ms && a->timer);
             }
         }
     }
 
     if (sync != NULL) {
         crm_debug("Broadcasting local-only values");
         attrd_send_message(NULL, sync, false);
         free_xml(sync);
     }
 }
 
 int
 attrd_cluster_connect(void)
 {
     int rc = pcmk_rc_ok;
 
     attrd_cluster = pcmk_cluster_new();
 
     pcmk_cluster_set_destroy_fn(attrd_cluster, attrd_cpg_destroy);
     pcmk_cpg_set_deliver_fn(attrd_cluster, attrd_cpg_dispatch);
     pcmk_cpg_set_confchg_fn(attrd_cluster, pcmk__cpg_confchg_cb);
 
     pcmk__cluster_set_status_callback(&attrd_peer_change_cb);
 
     rc = pcmk_cluster_connect(attrd_cluster);
     rc = pcmk_rc2legacy(rc);
     if (rc != pcmk_ok) {
         crm_err("Cluster connection failed");
         return rc;
     }
     return pcmk_ok;
 }
 
 void
 attrd_peer_clear_failure(pcmk__request_t *request)
 {
     xmlNode *xml = request->xml;
     const char *rsc = crm_element_value(xml, PCMK__XA_ATTR_RESOURCE);
     const char *host = crm_element_value(xml, PCMK__XA_ATTR_HOST);
     const char *op = crm_element_value(xml, PCMK__XA_ATTR_CLEAR_OPERATION);
     const char *interval_spec = crm_element_value(xml,
                                                   PCMK__XA_ATTR_CLEAR_INTERVAL);
     guint interval_ms = 0U;
     char *attr = NULL;
     GHashTableIter iter;
     regex_t regex;
 
     crm_node_t *peer = pcmk__get_node(0, request->peer, NULL,
                                       pcmk__node_search_cluster_member);
 
     pcmk_parse_interval_spec(interval_spec, &interval_ms);
 
     if (attrd_failure_regex(&regex, rsc, op, interval_ms) != pcmk_ok) {
         crm_info("Ignoring invalid request to clear failures for %s",
                  pcmk__s(rsc, "all resources"));
         return;
     }
 
     crm_xml_add(xml, PCMK_XA_TASK, PCMK__ATTRD_CMD_UPDATE);
 
     /* Make sure value is not set, so we delete */
     pcmk__xe_remove_attr(xml, PCMK__XA_ATTR_VALUE);
 
     g_hash_table_iter_init(&iter, attributes);
     while (g_hash_table_iter_next(&iter, (gpointer *) &attr, NULL)) {
         if (regexec(&regex, attr, 0, NULL, 0) == 0) {
             crm_trace("Matched %s when clearing %s",
                       attr, pcmk__s(rsc, "all resources"));
             crm_xml_add(xml, PCMK__XA_ATTR_NAME, attr);
             attrd_peer_update(peer, xml, host, false);
         }
     }
     regfree(&regex);
 }
 
 /*!
  * \internal
  * \brief Load attributes from a peer sync response
  *
  * \param[in]     peer      Peer that sent sync response
  * \param[in]     peer_won  Whether peer is the attribute writer
  * \param[in,out] xml       Request XML
  */
 void
 attrd_peer_sync_response(const crm_node_t *peer, bool peer_won, xmlNode *xml)
 {
     crm_info("Processing " PCMK__ATTRD_CMD_SYNC_RESPONSE " from %s",
              peer->uname);
 
     if (peer_won) {
         /* Initialize the "seen" flag for all attributes to cleared, so we can
          * detect attributes that local node has but the writer doesn't.
          */
         attrd_clear_value_seen();
     }
 
     // Process each attribute update in the sync response
     for (xmlNode *child = pcmk__xe_first_child(xml, NULL, NULL, NULL);
          child != NULL; child = pcmk__xe_next(child)) {
 
         attrd_peer_update(peer, child,
                           crm_element_value(child, PCMK__XA_ATTR_HOST), true);
     }
 
     if (peer_won) {
         /* If any attributes are still not marked as seen, the writer doesn't
          * know about them, so send all peers an update with them.
          */
         broadcast_unseen_local_values();
     }
 }
 
 /*!
  * \internal
  * \brief Remove all attributes and optionally peer cache entries for a node
  *
  * \param[in] host     Name of node to purge
  * \param[in] uncache  If true, remove node from peer caches
  * \param[in] source   Who requested removal (only used for logging)
  */
 void
 attrd_peer_remove(const char *host, bool uncache, const char *source)
 {
     attribute_t *a = NULL;
     GHashTableIter aIter;
 
     CRM_CHECK(host != NULL, return);
     crm_notice("Removing all %s attributes for node %s "
                CRM_XS " %s reaping node from cache",
                host, source, (uncache? "and" : "without"));
 
     g_hash_table_iter_init(&aIter, attributes);
     while (g_hash_table_iter_next(&aIter, NULL, (gpointer *) & a)) {
         if(g_hash_table_remove(a->values, host)) {
             crm_debug("Removed %s[%s] for peer %s", a->id, host, source);
         }
     }
 
     if (uncache) {
         pcmk__purge_node_from_cache(host, 0);
+        attrd_forget_node_xml_id(host);
     }
 }
 
 /*!
  * \internal
  * \brief Send all known attributes and values to a peer
  *
  * \param[in] peer  Peer to send sync to (if NULL, broadcast to all peers)
  */
 void
 attrd_peer_sync(crm_node_t *peer)
 {
     GHashTableIter aIter;
     GHashTableIter vIter;
 
     attribute_t *a = NULL;
     attribute_value_t *v = NULL;
     xmlNode *sync = pcmk__xe_create(NULL, __func__);
 
     crm_xml_add(sync, PCMK_XA_TASK, PCMK__ATTRD_CMD_SYNC_RESPONSE);
 
     g_hash_table_iter_init(&aIter, attributes);
     while (g_hash_table_iter_next(&aIter, NULL, (gpointer *) & a)) {
         g_hash_table_iter_init(&vIter, a->values);
         while (g_hash_table_iter_next(&vIter, NULL, (gpointer *) & v)) {
             crm_debug("Syncing %s[%s]='%s' to %s",
                       a->id, v->nodename, readable_value(v),
                       readable_peer(peer));
             attrd_add_value_xml(sync, a, v, false);
         }
     }
 
     crm_debug("Syncing values to %s", readable_peer(peer));
     attrd_send_message(peer, sync, false);
     free_xml(sync);
 }
 
 void
 attrd_peer_update(const crm_node_t *peer, xmlNode *xml, const char *host,
                   bool filter)
 {
     bool handle_sync_point = false;
 
     CRM_CHECK((peer != NULL) && (xml != NULL), return);
     if (xml->children != NULL) {
         for (xmlNode *child = pcmk__xe_first_child(xml, PCMK_XE_OP, NULL, NULL);
              child != NULL; child = pcmk__xe_next_same(child)) {
 
             pcmk__xe_copy_attrs(child, xml, pcmk__xaf_no_overwrite);
             attrd_peer_update_one(peer, child, filter);
 
             if (attrd_request_has_sync_point(child)) {
                 handle_sync_point = true;
             }
         }
 
     } else {
         attrd_peer_update_one(peer, xml, filter);
 
         if (attrd_request_has_sync_point(xml)) {
             handle_sync_point = true;
         }
     }
 
     /* If the update XML specified that the client wanted to wait for a sync
      * point, process that now.
      */
     if (handle_sync_point) {
         crm_trace("Hit local sync point for attribute update");
         attrd_ack_waitlist_clients(attrd_sync_point_local, xml);
     }
 }
diff --git a/daemons/attrd/attrd_nodes.c b/daemons/attrd/attrd_nodes.c
new file mode 100644
index 0000000000..8fb7797f2d
--- /dev/null
+++ b/daemons/attrd/attrd_nodes.c
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2024-2025 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <stdio.h>      // NULL
+#include <glib.h>       // GHashTable, etc.
+
+#include "pacemaker-attrd.h"
+
+// Track the last known node XML ID for each node name
+static GHashTable *node_xml_ids = NULL;
+
+/*!
+ * \internal
+ * \brief Get last known XML ID for a given node
+ *
+ * \param[in] node_name  Name of node to check
+ *
+ * \return Last known XML ID for node (or NULL if none known)
+ *
+ * \note The return value may become invalid if attrd_set_node_xml_id() or
+ *       attrd_forget_node_xml_id() is later called for \p node_name.
+ */
+const char *
+attrd_get_node_xml_id(const char *node_name)
+{
+    if (node_xml_ids == NULL) {
+        return NULL;
+    }
+    return g_hash_table_lookup(node_xml_ids, node_name);
+}
+
+/*!
+ * \internal
+ * \brief Set last known XML ID for a given node
+ *
+ * \param[in] node_name    Name of node to set
+ * \param[in] node_xml_id  New XML ID to set for node
+ */
+void
+attrd_set_node_xml_id(const char *node_name, const char *node_xml_id)
+{
+    if (node_xml_ids == NULL) {
+        node_xml_ids = pcmk__strikey_table(free, free);
+    }
+    pcmk__insert_dup(node_xml_ids, node_name, node_xml_id);
+}
+
+/*!
+ * \internal
+ * \brief Forget last known XML ID for a given node
+ *
+ * \param[in] node_name    Name of node to forget
+ */
+void
+attrd_forget_node_xml_id(const char *node_name)
+{
+    if (node_xml_ids == NULL) {
+        return;
+    }
+    g_hash_table_remove(node_xml_ids, node_name);
+}
+
+/*!
+ * \internal
+ * \brief Free the node XML ID cache
+ */
+void
+attrd_cleanup_xml_ids(void)
+{
+    if (node_xml_ids != NULL) {
+        g_hash_table_destroy(node_xml_ids);
+        node_xml_ids = NULL;
+    }
+}
diff --git a/daemons/attrd/attrd_utils.c b/daemons/attrd/attrd_utils.c
index 714a19fead..2d0bc76db9 100644
--- a/daemons/attrd/attrd_utils.c
+++ b/daemons/attrd/attrd_utils.c
@@ -1,303 +1,302 @@
 /*
  * Copyright 2004-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <stdio.h>
 #include <stdbool.h>
 #include <errno.h>
 #include <glib.h>
 #include <regex.h>
 #include <sys/types.h>
 
 #include <crm/crm.h>
 #include <crm/common/ipc_internal.h>
 #include <crm/common/mainloop.h>
 #include <crm/common/xml.h>
 
 #include "pacemaker-attrd.h"
 
 cib_t *the_cib = NULL;
 
 static bool requesting_shutdown = false;
 static bool shutting_down = false;
 static GMainLoop *mloop = NULL;
 
 /* A hash table storing information on the protocol version of each peer attrd.
  * The key is the peer's uname, and the value is the protocol version number.
  */
 GHashTable *peer_protocol_vers = NULL;
 
 /*!
  * \internal
  * \brief  Set requesting_shutdown state
  */
 void
 attrd_set_requesting_shutdown(void)
 {
     requesting_shutdown = true;
 }
 
 /*!
  * \internal
  * \brief  Clear requesting_shutdown state
  */
 void
 attrd_clear_requesting_shutdown(void)
 {
     requesting_shutdown = false;
 }
 
 /*!
  * \internal
  * \brief Check whether local attribute manager is shutting down
  *
  * \param[in] if_requested  If \c true, also consider presence of
  *                          \c PCMK__NODE_ATTR_SHUTDOWN attribute
  *
  * \return \c true if local attribute manager has begun shutdown sequence
  *         or (if \p if_requested is \c true) whether local node has a nonzero
  *         \c PCMK__NODE_ATTR_SHUTDOWN attribute set, otherwise \c false
  * \note Most callers should pass \c false for \p if_requested, because the
  *       attribute manager needs to continue performing while the controller is
  *       shutting down, and even needs to be eligible for election in case all
  *       nodes are shutting down.
  */
 bool
 attrd_shutting_down(bool if_requested)
 {
     return shutting_down || (if_requested && requesting_shutdown);
 }
 
 /*!
  * \internal
  * \brief  Exit (using mainloop or not, as appropriate)
  *
  * \param[in] nsig  Ignored
  */
 void
 attrd_shutdown(int nsig)
 {
     // Tell various functions not to do anthing
     shutting_down = true;
 
     // Don't respond to signals while shutting down
     mainloop_destroy_signal(SIGTERM);
     mainloop_destroy_signal(SIGCHLD);
     mainloop_destroy_signal(SIGPIPE);
     mainloop_destroy_signal(SIGUSR1);
     mainloop_destroy_signal(SIGUSR2);
     mainloop_destroy_signal(SIGTRAP);
 
     attrd_free_waitlist();
     attrd_free_confirmations();
 
     if (peer_protocol_vers != NULL) {
         g_hash_table_destroy(peer_protocol_vers);
         peer_protocol_vers = NULL;
     }
 
     if ((mloop == NULL) || !g_main_loop_is_running(mloop)) {
         /* If there's no main loop active, just exit. This should be possible
          * only if we get SIGTERM in brief windows at start-up and shutdown.
          */
         crm_exit(CRM_EX_OK);
     } else {
         g_main_loop_quit(mloop);
         g_main_loop_unref(mloop);
     }
 }
 
 /*!
  * \internal
  * \brief Create a main loop for attrd
  */
 void
 attrd_init_mainloop(void)
 {
     mloop = g_main_loop_new(NULL, FALSE);
 }
 
 /*!
  * \internal
  * \brief Run attrd main loop
  */
 void
 attrd_run_mainloop(void)
 {
     g_main_loop_run(mloop);
 }
 
 /* strlen("value") */
 #define plus_plus_len (5)
 
 /*!
  * \internal
  * \brief  Check whether an attribute value should be expanded
  *
  * \param[in] value  Attribute value to check
  *
  * \return true if value needs expansion, false otherwise
  */
 bool
 attrd_value_needs_expansion(const char *value)
 {
     return ((strlen(value) >= (plus_plus_len + 2))
            && (value[plus_plus_len] == '+')
            && ((value[plus_plus_len + 1] == '+')
                || (value[plus_plus_len + 1] == '=')));
 }
 
 /*!
  * \internal
  * \brief Expand an increment expression into an integer
  *
  * \param[in] value      Attribute increment expression to expand
  * \param[in] old_value  Previous value of attribute
  *
  * \return Expanded value
  */
 int
 attrd_expand_value(const char *value, const char *old_value)
 {
     int offset = 1;
     int int_value = char2score(old_value);
 
     if (value[plus_plus_len + 1] != '+') {
         const char *offset_s = value + (plus_plus_len + 2);
 
         offset = char2score(offset_s);
     }
     int_value += offset;
 
     if (int_value > PCMK_SCORE_INFINITY) {
         int_value = PCMK_SCORE_INFINITY;
     }
     return int_value;
 }
 
 /*!
  * \internal
  * \brief Create regular expression matching failure-related attributes
  *
  * \param[out] regex  Where to store created regular expression
  * \param[in]  rsc    Name of resource to clear (or NULL for all)
  * \param[in]  op     Operation to clear if rsc is specified (or NULL for all)
  * \param[in]  interval_ms  Interval of operation to clear if op is specified
  *
  * \return pcmk_ok on success, -EINVAL if arguments are invalid
  *
  * \note The caller is responsible for freeing the result with regfree().
  */
 int
 attrd_failure_regex(regex_t *regex, const char *rsc, const char *op,
                     guint interval_ms)
 {
     char *pattern = NULL;
     int rc;
 
     /* Create a pattern that matches desired attributes */
 
     if (rsc == NULL) {
         pattern = pcmk__str_copy(ATTRD_RE_CLEAR_ALL);
     } else if (op == NULL) {
         pattern = crm_strdup_printf(ATTRD_RE_CLEAR_ONE, rsc);
     } else {
         pattern = crm_strdup_printf(ATTRD_RE_CLEAR_OP, rsc, op, interval_ms);
     }
 
     /* Compile pattern into regular expression */
     crm_trace("Clearing attributes matching %s", pattern);
     rc = regcomp(regex, pattern, REG_EXTENDED|REG_NOSUB);
     free(pattern);
 
     return (rc == 0)? pcmk_ok : -EINVAL;
 }
 
 void
 attrd_free_attribute_value(gpointer data)
 {
     attribute_value_t *v = data;
 
     free(v->nodename);
     free(v->current);
     free(v->requested);
-    free(v->node_xml_id);
     free(v);
 }
 
 void
 attrd_free_attribute(gpointer data)
 {
     attribute_t *a = data;
     if(a) {
         free(a->id);
         free(a->set_id);
         free(a->set_type);
         free(a->user);
 
         mainloop_timer_del(a->timer);
         g_hash_table_destroy(a->values);
 
         free(a);
     }
 }
 
 /*!
  * \internal
  * \brief When a peer node leaves the cluster, stop tracking its protocol version.
  *
  * \param[in] host  The peer node's uname to be removed
  */
 void
 attrd_remove_peer_protocol_ver(const char *host)
 {
     if (peer_protocol_vers != NULL) {
         g_hash_table_remove(peer_protocol_vers, host);
     }
 }
 
 /*!
  * \internal
  * \brief When a peer node broadcasts a message with its protocol version, keep
  *        track of that information.
  *
  * We keep track of each peer's protocol version so we know which peers to
  * expect confirmation messages from when handling cluster-wide sync points.
  * We additionally keep track of the lowest protocol version supported by all
  * peers so we know when we can send IPC messages containing more than one
  * request.
  *
  * \param[in] host  The peer node's uname to be tracked
  * \param[in] value The peer node's protocol version
  */
 void
 attrd_update_minimum_protocol_ver(const char *host, const char *value)
 {
     int ver;
 
     if (peer_protocol_vers == NULL) {
         peer_protocol_vers = pcmk__strkey_table(free, NULL);
     }
 
     pcmk__scan_min_int(value, &ver, 0);
 
     if (ver > 0) {
         /* Record the peer attrd's protocol version. */
         g_hash_table_insert(peer_protocol_vers, pcmk__str_copy(host),
                             GINT_TO_POINTER(ver));
 
         /* If the protocol version is a new minimum, record it as such. */
         if (minimum_protocol_version == -1 || ver < minimum_protocol_version) {
             minimum_protocol_version = ver;
             crm_trace("Set minimum attrd protocol version to %d",
                       minimum_protocol_version);
         }
     }
 }
diff --git a/daemons/attrd/pacemaker-attrd.c b/daemons/attrd/pacemaker-attrd.c
index 4ae5c8a555..1d86c1b5cc 100644
--- a/daemons/attrd/pacemaker-attrd.c
+++ b/daemons/attrd/pacemaker-attrd.c
@@ -1,224 +1,226 @@
 /*
  * Copyright 2013-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #include <crm_internal.h>
 
 #include <sys/param.h>
 #include <stdio.h>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <unistd.h>
 
 #include <stdlib.h>
 #include <errno.h>
 #include <fcntl.h>
 
 #include <crm/crm.h>
 #include <crm/pengine/rules.h>
 #include <crm/common/cmdline_internal.h>
 #include <crm/common/iso8601.h>
 #include <crm/common/ipc.h>
 #include <crm/common/ipc_internal.h>
 #include <crm/common/output_internal.h>
 #include <crm/common/xml.h>
 #include <crm/cluster/internal.h>
 
 #include <crm/common/attrs_internal.h>
 #include "pacemaker-attrd.h"
 
 #define SUMMARY "daemon for managing Pacemaker node attributes"
 
 gboolean stand_alone = FALSE;
 gchar **log_files = NULL;
 
 static GOptionEntry entries[] = {
     { "stand-alone", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &stand_alone,
       "(Advanced use only) Run in stand-alone mode", NULL },
 
     { "logfile", 'l', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME_ARRAY,
       &log_files, "Send logs to the additional named logfile", NULL },
 
     { NULL }
 };
 
 static pcmk__output_t *out = NULL;
 
 static pcmk__supported_format_t formats[] = {
     PCMK__SUPPORTED_FORMAT_NONE,
     PCMK__SUPPORTED_FORMAT_TEXT,
     PCMK__SUPPORTED_FORMAT_XML,
     { NULL, NULL, NULL }
 };
 
 lrmd_t *the_lrmd = NULL;
 pcmk_cluster_t *attrd_cluster = NULL;
 crm_trigger_t *attrd_config_read = NULL;
 crm_exit_t attrd_exit_status = CRM_EX_OK;
 
 static bool
 ipc_already_running(void)
 {
     pcmk_ipc_api_t *old_instance = NULL;
     int rc = pcmk_rc_ok;
 
     rc = pcmk_new_ipc_api(&old_instance, pcmk_ipc_attrd);
     if (rc != pcmk_rc_ok) {
         return false;
     }
 
     rc = pcmk__connect_ipc(old_instance, pcmk_ipc_dispatch_sync, 2);
     if (rc != pcmk_rc_ok) {
         crm_debug("No existing %s manager instance found: %s",
                   pcmk_ipc_name(old_instance, true), pcmk_rc_str(rc));
         pcmk_free_ipc_api(old_instance);
         return false;
     }
 
     pcmk_disconnect_ipc(old_instance);
     pcmk_free_ipc_api(old_instance);
     return true;
 }
 
 static GOptionContext *
 build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
     GOptionContext *context = NULL;
 
     context = pcmk__build_arg_context(args, "text (default), xml", group, NULL);
     pcmk__add_main_args(context, entries);
     return context;
 }
 
 int
 main(int argc, char **argv)
 {
     int rc = pcmk_rc_ok;
 
     GError *error = NULL;
     bool initialized = false;
 
     GOptionGroup *output_group = NULL;
     pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
     gchar **processed_args = pcmk__cmdline_preproc(argv, NULL);
     GOptionContext *context = build_arg_context(args, &output_group);
 
     attrd_init_mainloop();
     crm_log_preinit(NULL, argc, argv);
     mainloop_add_signal(SIGTERM, attrd_shutdown);
 
     pcmk__register_formats(output_group, formats);
     if (!g_option_context_parse_strv(context, &processed_args, &error)) {
         attrd_exit_status = CRM_EX_USAGE;
         goto done;
     }
 
     rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
     if ((rc != pcmk_rc_ok) || (out == NULL)) {
         attrd_exit_status = CRM_EX_ERROR;
         g_set_error(&error, PCMK__EXITC_ERROR, attrd_exit_status,
                     "Error creating output format %s: %s",
                     args->output_ty, pcmk_rc_str(rc));
         goto done;
     }
 
     if (args->version) {
         out->version(out, false);
         goto done;
     }
 
     // Open additional log files
     pcmk__add_logfiles(log_files, out);
 
     crm_log_init(PCMK__VALUE_ATTRD, LOG_INFO, TRUE, FALSE, argc, argv, FALSE);
     crm_notice("Starting Pacemaker node attribute manager%s",
                stand_alone ? " in standalone mode" : "");
 
     if (ipc_already_running()) {
         const char *msg = "pacemaker-attrd is already active, aborting startup";
 
         attrd_exit_status = CRM_EX_OK;
         g_set_error(&error, PCMK__EXITC_ERROR, attrd_exit_status, "%s", msg);
         crm_err("%s", msg);
         goto done;
     }
 
     initialized = true;
 
     attributes = pcmk__strkey_table(NULL, attrd_free_attribute);
 
     /* Connect to the CIB before connecting to the cluster or listening for IPC.
      * This allows us to assume the CIB is connected whenever we process a
      * cluster or IPC message (which also avoids start-up race conditions).
      */
     if (!stand_alone) {
         if (attrd_cib_connect(30) != pcmk_ok) {
             attrd_exit_status = CRM_EX_FATAL;
             g_set_error(&error, PCMK__EXITC_ERROR, attrd_exit_status,
                         "Could not connect to the CIB");
             goto done;
         }
         crm_info("CIB connection active");
     }
 
     if (attrd_cluster_connect() != pcmk_ok) {
         attrd_exit_status = CRM_EX_FATAL;
         g_set_error(&error, PCMK__EXITC_ERROR, attrd_exit_status,
                     "Could not connect to the cluster");
         goto done;
     }
     crm_info("Cluster connection active");
 
     // Initialization that requires the cluster to be connected
     attrd_election_init();
 
     if (!stand_alone) {
         attrd_cib_init();
     }
 
     /* Set a private attribute for ourselves with the protocol version we
      * support. This lets all nodes determine the minimum supported version
      * across all nodes. It also ensures that the writer learns our node name,
      * so it can send our attributes to the CIB.
      */
     attrd_broadcast_protocol();
 
     attrd_init_ipc();
     crm_notice("Pacemaker node attribute manager successfully started and accepting connections");
     attrd_run_mainloop();
 
   done:
     if (initialized) {
         crm_info("Shutting down attribute manager");
 
         attrd_election_fini();
         attrd_ipc_fini();
         attrd_lrmd_disconnect();
 
         if (!stand_alone) {
             attrd_cib_disconnect();
         }
 
         attrd_free_waitlist();
         pcmk_cluster_free(attrd_cluster);
         g_hash_table_destroy(attributes);
     }
 
+    attrd_cleanup_xml_ids();
+
     g_strfreev(processed_args);
     pcmk__free_arg_context(context);
 
     g_strfreev(log_files);
 
     pcmk__output_and_clear_error(&error, out);
 
     if (out != NULL) {
         out->finish(out, attrd_exit_status, true, NULL);
         pcmk__output_free(out);
     }
     pcmk__unregister_formats();
     crm_exit(attrd_exit_status);
 }
diff --git a/daemons/attrd/pacemaker-attrd.h b/daemons/attrd/pacemaker-attrd.h
index 65bac62950..783c0332fc 100644
--- a/daemons/attrd/pacemaker-attrd.h
+++ b/daemons/attrd/pacemaker-attrd.h
@@ -1,259 +1,265 @@
 /*
  * Copyright 2013-2024 the Pacemaker project contributors
  *
  * The version control history for this file may have further details.
  *
  * This source code is licensed under the GNU General Public License version 2
  * or later (GPLv2+) WITHOUT ANY WARRANTY.
  */
 
 #ifndef PACEMAKER_ATTRD__H
 #  define PACEMAKER_ATTRD__H
 
 #include <regex.h>
 #include <glib.h>
 #include <crm/crm.h>
 #include <crm/cluster.h>
 #include <crm/cluster/election_internal.h>
 #include <crm/common/messages_internal.h>
 #include <crm/cib/cib_types.h>
 
 /*
  * Legacy attrd (all pre-1.1.11 Pacemaker versions, plus all versions when used
  * with the no-longer-supported CMAN or corosync-plugin stacks) is unversioned.
  *
  * With atomic attrd, each attrd will send ATTRD_PROTOCOL_VERSION with every
  * peer request and reply. As of Pacemaker 2.0.0, at start-up each attrd will
  * also set a private attribute for itself with its version, so any attrd can
  * determine the minimum version supported by all peers.
  *
  * Protocol  Pacemaker  Significant changes
  * --------  ---------  -------------------
  *     1       1.1.11   PCMK__ATTRD_CMD_UPDATE (PCMK__XA_ATTR_NAME only),
  *                      PCMK__ATTRD_CMD_PEER_REMOVE, PCMK__ATTRD_CMD_REFRESH,
  *                      PCMK__ATTRD_CMD_FLUSH, PCMK__ATTRD_CMD_SYNC_RESPONSE
  *     1       1.1.13   PCMK__ATTRD_CMD_UPDATE (with PCMK__XA_ATTR_REGEX),
  *                      PCMK__ATTRD_CMD_QUERY
  *     1       1.1.15   PCMK__ATTRD_CMD_UPDATE_BOTH,
  *                      PCMK__ATTRD_CMD_UPDATE_DELAY
  *     2       1.1.17   PCMK__ATTRD_CMD_CLEAR_FAILURE
  *     3       2.1.1    PCMK__ATTRD_CMD_SYNC_RESPONSE indicates remote nodes
  *     4       2.1.5    Multiple attributes can be updated in a single IPC
  *                      message
  *     5       2.1.5    Peers can request confirmation of a sent message
  *     6       2.1.7    PCMK__ATTRD_CMD_PEER_REMOVE supports PCMK__XA_REAP
  */
 #define ATTRD_PROTOCOL_VERSION "6"
 
 #define ATTRD_SUPPORTS_MULTI_MESSAGE(x) ((x) >= 4)
 #define ATTRD_SUPPORTS_CONFIRMATION(x)  ((x) >= 5)
 
 #define attrd_send_ack(client, id, flags)                       \
     pcmk__ipc_send_ack((client), (id), (flags), PCMK__XE_ACK,   \
                        ATTRD_PROTOCOL_VERSION, CRM_EX_INDETERMINATE)
 
 void attrd_init_mainloop(void);
 void attrd_run_mainloop(void);
 
 void attrd_set_requesting_shutdown(void);
 void attrd_clear_requesting_shutdown(void);
 void attrd_free_waitlist(void);
 bool attrd_shutting_down(bool if_requested);
 void attrd_shutdown(int nsig);
 void attrd_init_ipc(void);
 void attrd_ipc_fini(void);
 
 int attrd_cib_connect(int max_retry);
 void attrd_cib_disconnect(void);
 void attrd_cib_init(void);
 void attrd_cib_erase_transient_attrs(const char *node);
 
 bool attrd_value_needs_expansion(const char *value);
 int attrd_expand_value(const char *value, const char *old_value);
 
 /* regular expression to clear failures of all resources */
 #define ATTRD_RE_CLEAR_ALL \
     "^(" PCMK__FAIL_COUNT_PREFIX "|" PCMK__LAST_FAILURE_PREFIX ")-"
 
 /* regular expression to clear failure of all operations for one resource
  * (format takes resource name)
  *
  * @COMPAT attributes set < 1.1.17:
  * also match older attributes that do not have the operation part
  */
 #define ATTRD_RE_CLEAR_ONE ATTRD_RE_CLEAR_ALL "%s(#.+_[0-9]+)?$"
 
 /* regular expression to clear failure of one operation for one resource
  * (format takes resource name, operation name, and interval)
  *
  * @COMPAT attributes set < 1.1.17:
  * also match older attributes that do not have the operation part
  */
 #define ATTRD_RE_CLEAR_OP ATTRD_RE_CLEAR_ALL "%s(#%s_%u)?$"
 
 int attrd_failure_regex(regex_t *regex, const char *rsc, const char *op,
                         guint interval_ms);
 
 extern cib_t *the_cib;
 extern crm_exit_t attrd_exit_status;
 
 /* Alerts */
 
 extern lrmd_t *the_lrmd;
 extern crm_trigger_t *attrd_config_read;
 
 void attrd_lrmd_disconnect(void);
 gboolean attrd_read_options(gpointer user_data);
 int attrd_send_attribute_alert(const char *node, const char *node_xml_id,
                                const char *attr, const char *value);
 
 // Elections
 void attrd_election_init(void);
 void attrd_election_fini(void);
 void attrd_start_election_if_needed(void);
 bool attrd_election_won(void);
 void attrd_handle_election_op(const crm_node_t *peer, xmlNode *xml);
 bool attrd_check_for_new_writer(const crm_node_t *peer, const xmlNode *xml);
 void attrd_declare_winner(void);
 void attrd_remove_voter(const crm_node_t *peer);
 void attrd_xml_add_writer(xmlNode *xml);
 
 enum attrd_attr_flags {
     attrd_attr_none         = 0U,
 
     // At least one of attribute's values has changed since last write
     attrd_attr_changed      = (1U << 0),
 
     // At least one of attribute's values has an unknown node XML ID
     attrd_attr_node_unknown = (1U << 1),
 
     // This attribute should never be written to the CIB
     attrd_attr_is_private   = (1U << 2),
 
     // Ignore any configured delay for next write of this attribute
     attrd_attr_force_write  = (1U << 3),
 };
 
 typedef struct attribute_s {
     char *id;       // Attribute name
     char *set_type; // PCMK_XE_INSTANCE_ATTRIBUTES or PCMK_XE_UTILIZATION
     char *set_id;   // Set's XML ID to use when writing
     char *user;     // ACL user to use for CIB writes
     int update;     // Call ID of pending write
     int timeout_ms; // How long to wait for more changes before writing
     uint32_t flags; // Group of enum attrd_attr_flags
     GHashTable *values;         // Key: node name, value: attribute_value_t
     mainloop_timer_t *timer;    // Timer to use for timeout_ms
 } attribute_t;
 
 #define attrd_set_attr_flags(attr, flags_to_set) do {               \
         (attr)->flags = pcmk__set_flags_as(__func__, __LINE__,      \
             LOG_TRACE, "Value for attribute", (attr)->id,           \
             (attr)->flags, (flags_to_set), #flags_to_set);          \
     } while (0)
 
 #define attrd_clear_attr_flags(attr, flags_to_clear) do {           \
         (attr)->flags = pcmk__clear_flags_as(__func__, __LINE__,    \
             LOG_TRACE, "Value for attribute", (attr)->id,           \
             (attr)->flags, (flags_to_clear), #flags_to_clear);      \
     } while (0)
 
 enum attrd_value_flags {
     attrd_value_none        = 0U,
     attrd_value_remote      = (1U << 0),  // Value is for Pacemaker Remote node
     attrd_value_from_peer   = (1U << 1),  // Value is from peer sync response
 };
 
 typedef struct attribute_value_s {
     char *nodename;     // Node that this value is for
     char *current;      // Attribute value
     char *requested;    // Value specified in pending CIB write, if any
     char *node_xml_id;  // XML ID used for node in CIB
     uint32_t flags;     // Group of attrd_value_flags
 } attribute_value_t;
 
 #define attrd_set_value_flags(attr_value, flags_to_set) do {            \
         (attr_value)->flags = pcmk__set_flags_as(__func__, __LINE__,    \
             LOG_TRACE, "Value for node", (attr_value)->nodename,        \
             (attr_value)->flags, (flags_to_set), #flags_to_set);        \
     } while (0)
 
 #define attrd_clear_value_flags(attr_value, flags_to_clear) do {        \
         (attr_value)->flags = pcmk__clear_flags_as(__func__, __LINE__,  \
             LOG_TRACE, "Value for node", (attr_value)->nodename,        \
             (attr_value)->flags, (flags_to_clear), #flags_to_clear);    \
     } while (0)
 
 extern pcmk_cluster_t *attrd_cluster;
 extern GHashTable *attributes;
 extern GHashTable *peer_protocol_vers;
 
 #define CIB_OP_TIMEOUT_S 120
 
 int attrd_cluster_connect(void);
 void attrd_broadcast_value(const attribute_t *a, const attribute_value_t *v);
 void attrd_peer_update(const crm_node_t *peer, xmlNode *xml, const char *host,
                        bool filter);
 void attrd_peer_sync(crm_node_t *peer);
 void attrd_peer_remove(const char *host, bool uncache, const char *source);
 void attrd_peer_clear_failure(pcmk__request_t *request);
 void attrd_peer_sync_response(const crm_node_t *peer, bool peer_won,
                               xmlNode *xml);
 
 void attrd_broadcast_protocol(void);
 xmlNode *attrd_client_peer_remove(pcmk__request_t *request);
 xmlNode *attrd_client_clear_failure(pcmk__request_t *request);
 xmlNode *attrd_client_update(pcmk__request_t *request);
 xmlNode *attrd_client_refresh(pcmk__request_t *request);
 xmlNode *attrd_client_query(pcmk__request_t *request);
 gboolean attrd_send_message(crm_node_t *node, xmlNode *data, bool confirm);
 
 xmlNode *attrd_add_value_xml(xmlNode *parent, const attribute_t *a,
                              const attribute_value_t *v, bool force_write);
 void attrd_clear_value_seen(void);
 void attrd_free_attribute(gpointer data);
 void attrd_free_attribute_value(gpointer data);
 attribute_t *attrd_populate_attribute(xmlNode *xml, const char *attr);
 char *attrd_set_id(const attribute_t *attr, const char *node_state_id);
 char *attrd_nvpair_id(const attribute_t *attr, const char *node_state_id);
 
 enum attrd_write_options {
     attrd_write_changed         = 0,
     attrd_write_all             = (1 << 0),
     attrd_write_no_delay        = (1 << 1),
 };
 
 void attrd_write_attributes(uint32_t options);
 void attrd_write_or_elect_attribute(attribute_t *a);
 
 extern int minimum_protocol_version;
 void attrd_remove_peer_protocol_ver(const char *host);
 void attrd_update_minimum_protocol_ver(const char *host, const char *value);
 
 mainloop_timer_t *attrd_add_timer(const char *id, int timeout_ms, attribute_t *attr);
 
 void attrd_unregister_handlers(void);
 void attrd_handle_request(pcmk__request_t *request);
 
 enum attrd_sync_point {
     attrd_sync_point_local,
     attrd_sync_point_cluster,
 };
 
 typedef int (*attrd_confirmation_action_fn)(xmlNode *);
 
 void attrd_add_client_to_waitlist(pcmk__request_t *request);
 void attrd_ack_waitlist_clients(enum attrd_sync_point sync_point, const xmlNode *xml);
 int attrd_cluster_sync_point_update(xmlNode *xml);
 void attrd_do_not_expect_from_peer(const char *host);
 void attrd_do_not_wait_for_client(pcmk__client_t *client);
 void attrd_expect_confirmations(pcmk__request_t *request, attrd_confirmation_action_fn fn);
 void attrd_free_confirmations(void);
 void attrd_handle_confirmation(int callid, const char *host);
 void attrd_remove_client_from_waitlist(pcmk__client_t *client);
 const char *attrd_request_sync_point(xmlNode *xml);
 bool attrd_request_has_sync_point(xmlNode *xml);
 
 extern gboolean stand_alone;
 
+// Node utilities (from attrd_nodes.c)
+const char *attrd_get_node_xml_id(const char *node_name);
+void attrd_set_node_xml_id(const char *node_name, const char *node_xml_id);
+void attrd_forget_node_xml_id(const char *node_name);
+void attrd_cleanup_xml_ids(void);
+
 #endif /* PACEMAKER_ATTRD__H */