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:
- newly added contacts were not scheduling persistence the same way updated contacts were
- 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:
- start with a clean runtime state tree
- allow a new contact to be discovered or added
- verify that advert or contact-related runtime activity occurs
- inspect whether
contacts3 is created or updated
- 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
- Should newly added contacts always schedule
dirty_contacts_expiry, not just updated contacts?
- Is excluding
ADV_TYPE_NONE from persistence always correct, or are there valid donor-native cases where such contacts should still survive into contacts3?
- 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.
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 intocontacts3.Relevant upstream repositories:
Observed behavior before patch and rebuild:
/channels/contacts3was absent or not updated for the newly discovered contactObserved behavior after patch and rebuild:
contacts3was created and updatedEnvironment
examples/companion_radioexamples/companion_radio/MyMesh.cppexamples/companion_radio/DataStore.cppSuspected Root Cause
Two donor-side behaviors appeared relevant during debugging:
contacts3Relevant 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:
The local helper used during debugging captured the prior logic as effectively only scheduling persistence for non-new 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.cpppersists contacts through a filterable save path:The local debugging helper captured the prior save filter body as:
During debugging, replacing that with an unconditional save path was part of what made contact persistence behave as expected:
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:
contacts3is created or updatedFailure case observed:
contacts3was absent or missing the new contactSuccess case observed after patch and rebuild:
contacts3was created or updated as expectedQuestions
dirty_contacts_expiry, not just updated contacts?ADV_TYPE_NONEfrom persistence always correct, or are there valid donor-native cases where such contacts should still survive intocontacts3?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.