Skip to content

companion radio: newly added contacts can fail to persist to contacts3 until runtime source is patched and rebuilt #2809

Description

@emergencyhamnet

While validating a Linux-hosted companion_radio-derived runtime, we hit a contact persistence failure where newly discovered contacts were visible in runtime activity but were not being persisted into contacts3.

Relevant upstream repositories:

Observed behavior before patch and rebuild:

  • advert and contact-related runtime activity was visible
  • the runtime could observe contact discovery-related events
  • advert-related state artifacts were present under the runtime state tree
  • but /channels/contacts3 was absent or not updated for the newly discovered contact

Observed behavior after patch and rebuild:

  • contacts3 was created and updated
  • the discovered contact persisted correctly
  • direct-send behavior improved because the contact record now survived persistence

Environment

  • donor behavior source: examples/companion_radio
  • affected donor source areas:
    • examples/companion_radio/MyMesh.cpp
    • examples/companion_radio/DataStore.cpp
  • runtime hosted on Linux in a companion-style port
  • managed runtime used a compiled binary, so donor source edits had no effect until the binary was rebuilt and restarted

Suspected Root Cause

Two donor-side behaviors appeared relevant during debugging:

  1. newly added contacts were not scheduling persistence the same way updated contacts were
  2. the contact save filter appeared to exclude some contacts that still needed to survive into contacts3

Relevant Source Context

1. New contacts should schedule persistence too

In the contact add or update path in examples/companion_radio/MyMesh.cpp, both update and add branches should schedule persistence.

Current live context after patching looked like this:

if (recipient) {
  updateContactFromFrame(*recipient, last_mod, cmd_frame, len);
  recipient->lastmod = last_mod;
  dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
  writeOKFrame();
} else {
  ContactInfo contact;
  updateContactFromFrame(contact, last_mod, cmd_frame, len);
  contact.lastmod = last_mod;
  contact.sync_since = 0;
  if (addContact(contact)) {
    dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
    writeOKFrame();
  } else {
    writeErrFrame(ERR_CODE_TABLE_FULL);
  }
}

The local helper used during debugging captured the prior logic as effectively only scheduling persistence for non-new contacts:

if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[]

This looked like a plausible reason that newly added contacts could remain in RAM for the current session but never be persisted.

2. The save filter may be too aggressive

DataStore.cpp persists contacts through a filterable save path:

void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c)) {
  File file = openWrite(_getContactsChannelsFS(), "/contacts3");
  if (file) {
    uint32_t idx = 0;
    ContactInfo c;
    uint8_t unused = 0;

    while (host->getContactForSave(idx, c)) {
      if (filter && !filter(c)) {
        idx++;  // advance to next contact

The local debugging helper captured the prior save filter body as:

static bool save_filter(const ContactInfo& c) {
  return c.type != ADV_TYPE_NONE;   // don't save the transient/anon entries
}

During debugging, replacing that with an unconditional save path was part of what made contact persistence behave as expected:

static bool save_filter(const ContactInfo& c) {
  (void)c;
  return true;
}

This suggests the save filter may be excluding contact records that are still needed for later direct messaging, path tracking, or eventual metadata promotion.

Reproduction Outline

This is the rough reproduction shape we observed:

  1. start with a clean runtime state tree
  2. allow a new contact to be discovered or added
  3. verify that advert or contact-related runtime activity occurs
  4. inspect whether contacts3 is created or updated
  5. restart the runtime and verify whether the contact survives persistence

Failure case observed:

  • runtime activity indicated that the contact had been seen
  • but contacts3 was absent or missing the new contact

Success case observed after patch and rebuild:

  • contacts3 was created or updated as expected
  • the contact survived persistence

Questions

  1. Should newly added contacts always schedule dirty_contacts_expiry, not just updated contacts?
  2. Is excluding ADV_TYPE_NONE from persistence always correct, or are there valid donor-native cases where such contacts should still survive into contacts3?
  3. Is there an expected path where a newly discovered contact should persist before its type is fully promoted?

Additional Note

In our case, donor source edits alone did not change runtime behavior because the managed Linux runtime was using a compiled binary. Behavior changed only after rebuilding and restarting the runtime binary.

That rebuild requirement is deployment-specific, but it was important during debugging because it initially looked like the donor source changes were ineffective when in fact the running binary was stale.

Why This Matters

If newly discovered contacts are visible transiently but do not persist into contacts3, the runtime can appear to receive adverts and partially discover nodes while still failing to retain usable contact state for later direct messaging or node interaction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions