Modify radio contact fill logic to use sent OR received messages as recency queue for loadin selection after favorites

This commit is contained in:
Jack Kingsman
2026-04-12 23:45:43 -07:00
parent e5e9eab935
commit 485df05372
3 changed files with 156 additions and 13 deletions

View File

@@ -1295,7 +1295,13 @@ async def stop_background_contact_reconciliation() -> None:
async def get_contacts_selected_for_radio_sync() -> list[Contact]:
"""Return the contacts that would be loaded onto the radio right now."""
"""Return the contacts that would be loaded onto the radio right now.
Fill order:
1. Favorites (up to full capacity)
2. Most recently DM-active non-repeaters (sent or received, up to 80% refill target)
3. Most recently advertised non-repeaters (up to 80% refill target)
"""
app_settings = await AppSettingsRepository.get()
max_contacts = _effective_radio_capacity(app_settings.max_radio_contacts)
refill_target, _full_sync_trigger = _compute_radio_contact_limits(max_contacts)
@@ -1315,7 +1321,7 @@ async def get_contacts_selected_for_radio_sync() -> list[Contact]:
break
if len(selected_contacts) < refill_target:
for contact in await ContactRepository.get_recently_contacted_non_repeaters(
for contact in await ContactRepository.get_recently_dm_active_non_repeaters(
limit=max_contacts
):
key = contact.public_key.lower()
@@ -1354,8 +1360,8 @@ async def _sync_contacts_to_radio_inner(mc: MeshCore) -> dict:
Fill order is:
1. Favorite contacts
2. Most recently interacted-with non-repeaters
3. Most recently advert-heard non-repeaters without interaction history
2. Most recently DM-active non-repeaters (sent or received)
3. Most recently advert-heard non-repeaters
Favorite contacts are always reloaded first, up to the configured capacity.
Additional non-favorite fill stops at the refill target (80% of capacity).
@@ -1489,8 +1495,8 @@ async def sync_recent_contacts_to_radio(force: bool = False, mc: MeshCore | None
"""
Load contacts to the radio for DM ACK support.
Fill order is favorites, then recently contacted non-repeaters,
then recently advert-heard non-repeaters. Favorites are always reloaded
Fill order is favorites, then recently DM-active non-repeaters (sent or
received), then recently advert-heard non-repeaters. Favorites are always reloaded
up to the configured capacity; additional non-favorite fill stops at the
80% refill target.
Only runs at most once every CONTACT_SYNC_THROTTLE_SECONDS unless forced.

View File

@@ -294,6 +294,28 @@ class ContactRepository:
rows = await cursor.fetchall()
return [ContactRepository._row_to_contact(row) for row in rows]
@staticmethod
async def get_recently_dm_active_non_repeaters(limit: int = 200) -> list[Contact]:
"""Get non-repeater contacts with the most recent DM activity (sent or received)."""
cursor = await db.conn.execute(
"""
SELECT c.*
FROM contacts c
INNER JOIN (
SELECT conversation_key, MAX(received_at) AS last_dm
FROM messages
WHERE type = 'PRIV'
GROUP BY conversation_key
) m ON c.public_key = m.conversation_key
WHERE c.type != 2 AND length(c.public_key) = 64
ORDER BY m.last_dm DESC
LIMIT ?
""",
(limit,),
)
rows = await cursor.fetchall()
return [ContactRepository._row_to_contact(row) for row in rows]
@staticmethod
async def get_recently_advertised_non_repeaters(limit: int = 200) -> list[Contact]:
"""Get recently advert-heard non-repeater contacts."""

View File

@@ -377,14 +377,22 @@ class TestSyncRecentContactsToRadio:
assert result["loaded"] == 2
@pytest.mark.asyncio
async def test_fills_remaining_slots_with_recently_contacted_then_advertised(self, test_db):
"""Fill order is favorites, then recent contacts, then recent adverts."""
await _insert_contact(KEY_A, "Alice", last_contacted=100)
await _insert_contact(KEY_B, "Bob", last_contacted=2000)
await _insert_contact("cc" * 32, "Carol", last_contacted=1000)
async def test_fills_remaining_slots_with_dm_active_then_advertised(self, test_db):
"""Fill order is favorites, then DM-active contacts, then recent adverts."""
await _insert_contact(KEY_A, "Alice")
await _insert_contact(KEY_B, "Bob")
await _insert_contact("cc" * 32, "Carol")
await _insert_contact("dd" * 32, "Dave", last_advert=3000)
await _insert_contact("ee" * 32, "Eve", last_advert=2500)
# Create DM activity for Alice (oldest), Bob (most recent), Carol (middle)
for key, ts in [(KEY_A, 100), (KEY_B, 2000), ("cc" * 32, 1000)]:
await test_db.conn.execute(
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES ('PRIV', ?, 'hi', ?)",
(key, ts),
)
await test_db.conn.commit()
await AppSettingsRepository.update(max_radio_contacts=5)
await ContactRepository.set_favorite(KEY_A, True)
@@ -401,6 +409,7 @@ class TestSyncRecentContactsToRadio:
loaded_keys = [
call.args[0]["public_key"] for call in mock_mc.commands.add_contact.call_args_list
]
# Alice (favorite), then Bob & Carol (DM-active, most recent first), then Dave (advert)
assert loaded_keys == [KEY_A, KEY_B, "cc" * 32, "dd" * 32]
@pytest.mark.asyncio
@@ -509,8 +518,15 @@ class TestSyncAndOffloadAll:
@pytest.mark.asyncio
async def test_duplicate_favorite_not_loaded_twice(self, test_db):
"""Duplicate favorite entries still load the contact only once."""
await _insert_contact(KEY_A, "Alice", last_contacted=2000)
await _insert_contact(KEY_B, "Bob", last_contacted=1000)
await _insert_contact(KEY_A, "Alice")
await _insert_contact(KEY_B, "Bob")
# Bob has DM activity so he appears in tier 2
await test_db.conn.execute(
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES ('PRIV', ?, 'hi', 1000)",
(KEY_B,),
)
await test_db.conn.commit()
await AppSettingsRepository.update(max_radio_contacts=2)
await ContactRepository.set_favorite(KEY_A, True)
@@ -1862,3 +1878,102 @@ class TestCollectRepeaterTelemetryLpp:
await _collect_repeater_telemetry(mc, contact)
assert "lpp_sensors" not in recorded_data
# ---------------------------------------------------------------------------
# get_contacts_selected_for_radio_sync — DM-active prioritization
# ---------------------------------------------------------------------------
class TestContactSelectionDmActive:
"""Verify that tier 2 prioritizes contacts with recent DM activity."""
@pytest.mark.asyncio
async def test_incoming_dm_contact_selected_over_advert_only(self, test_db):
"""A contact who sent us a DM should be prioritized over one who only advertised."""
from app.radio_sync import get_contacts_selected_for_radio_sync
# Create two non-repeater contacts
dm_sender_key = "aa" * 32
advert_only_key = "bb" * 32
await test_db.conn.execute(
"INSERT INTO contacts (public_key, name, type, last_seen, last_advert) VALUES (?, ?, 1, 100, 100)",
(dm_sender_key, "DM Sender"),
)
await test_db.conn.execute(
"INSERT INTO contacts (public_key, name, type, last_seen, last_advert) VALUES (?, ?, 1, 200, 200)",
(advert_only_key, "Advert Only"),
)
# DM Sender sent us a message (incoming DM)
await test_db.conn.execute(
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES ('PRIV', ?, 'hello', 300)",
(dm_sender_key,),
)
await test_db.conn.commit()
with patch(
"app.radio_sync.AppSettingsRepository.get",
new_callable=AsyncMock,
return_value=MagicMock(max_radio_contacts=200, tracked_telemetry_repeaters=[]),
):
selected = await get_contacts_selected_for_radio_sync()
keys = [c.public_key for c in selected]
assert dm_sender_key in keys
assert advert_only_key in keys
# DM Sender should come before Advert Only (tier 2 before tier 3)
assert keys.index(dm_sender_key) < keys.index(advert_only_key)
@pytest.mark.asyncio
async def test_outgoing_dm_contact_also_selected(self, test_db):
"""A contact we sent a DM to should also appear via DM-active tier."""
from app.radio_sync import get_contacts_selected_for_radio_sync
contact_key = "cc" * 32
await test_db.conn.execute(
"INSERT INTO contacts (public_key, name, type) VALUES (?, ?, 1)",
(contact_key, "Outgoing Target"),
)
await test_db.conn.execute(
"INSERT INTO messages (type, conversation_key, text, received_at, outgoing) VALUES ('PRIV', ?, 'hey', 300, 1)",
(contact_key,),
)
await test_db.conn.commit()
with patch(
"app.radio_sync.AppSettingsRepository.get",
new_callable=AsyncMock,
return_value=MagicMock(max_radio_contacts=200, tracked_telemetry_repeaters=[]),
):
selected = await get_contacts_selected_for_radio_sync()
keys = [c.public_key for c in selected]
assert contact_key in keys
@pytest.mark.asyncio
async def test_repeaters_excluded_from_dm_active_tier(self, test_db):
"""Repeater contacts should not appear in tier 2 even with DM activity."""
from app.radio_sync import get_contacts_selected_for_radio_sync
repeater_key = "dd" * 32
await test_db.conn.execute(
"INSERT INTO contacts (public_key, name, type) VALUES (?, ?, 2)",
(repeater_key, "Repeater"),
)
await test_db.conn.execute(
"INSERT INTO messages (type, conversation_key, text, received_at) VALUES ('PRIV', ?, 'cmd', 300)",
(repeater_key,),
)
await test_db.conn.commit()
with patch(
"app.radio_sync.AppSettingsRepository.get",
new_callable=AsyncMock,
return_value=MagicMock(max_radio_contacts=200, tracked_telemetry_repeaters=[]),
):
selected = await get_contacts_selected_for_radio_sync()
keys = [c.public_key for c in selected]
assert repeater_key not in keys