Skip to content

Evict stale radio contacts when the contact table is full - #5

Merged
sg merged 1 commit into
mainfrom
fix/contacts-rollover
Jun 11, 2026
Merged

sg merged 1 commit into
mainfrom
fix/contacts-rollover

Conversation

@sg

@sg sg commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Problem

The radio's contact table (~350 slots) doesn't age out old contacts. Once full, the firmware just emits CONTACTS_FULL and drops the new node's advert — so new contacts never replace stale ones, and the bot logs CONTACTS_FULL indefinitely. The bot's sqlite contacts table holds 500 and acts as the long-term archive; the radio is the working set.

Solution

When enabled, the bot evicts the stalest contacts from the radio to keep radio_evict_headroom free slots:

  • at startup, after device-info/contacts sync;
  • automatically on CONTACTS_FULL (debounced);
  • on-demand from the web UI Manage → Radio page.

Eviction operates on a fresh get_contacts(lastmod=0) dump (the DB archive may hold contacts no longer on the radio, so it can't choose victims) and never deletes DB rows — the archive is preserved. Victims are stalest-first by a fused key: bot-DB last_synced_at → radio lastmod → a plausible last_advert (garbage sender-clock timestamps are treated as oldest). Protected: configured owners, all bot users, anyone the bot DM'd in the last 24h, and any contact types in radio_evict_protect_types. Removal is capped per run and aborts after 3 consecutive radio errors; the meshcore lib's in-memory contact cache is kept in step so send_dm won't reference an evicted entry.

Config ([bot])

key default meaning
radio_evict_enabled true auto triggers (startup + CONTACTS_FULL)
radio_evict_headroom 8 keep ≥ N free slots
radio_evict_protect_types (none) contact types never evicted (names/ints)
radio_evict_max_per_run 50 safety cap per run
radio_evict_min_interval 120 seconds debounce for auto runs

Manual eviction works even when disabled; the switch only governs the automatic triggers. Web policy changes are runtime-only — mcbot.conf is the startup source of truth.

Web UI / API

  • GET /api/radio/contacts-status, POST /api/radio/evict-contacts, POST /api/radio/contacts-policy (auth required).
  • Radio tab "Contact table" card: used/max/free + protected/evictable counts, a read-only firmware autoadd-byte diagnostic, an auto-evict toggle + headroom input, and an "Evict now" button that previews victims (dry-run) before confirming. Rebuilt static assets included.

Notes

  • The firmware-native set_autoadd_config is not exposed — its flag byte is undocumented (firmware source/web research were unavailable). Only a read-only autoadd_raw diagnostic is surfaced; the setter can be added later once the semantics are verified.
  • "No headroom reached" is reported as a shortfall (e.g. when protections exceed capacity), never an error.

Testing

tests/test_radio_eviction.py (run with a deps-equipped Python) — victim selection ordering/protection/garbage-timestamp handling, dry-run, headroom math, per-run cap, 3-error abort, shortfall, DB-archive preservation, and the disabled-policy handler gate. All pass; existing tests/test_repeat_tracking.py still passes; web-ui builds clean.

🤖 Generated with Claude Code

The radio's contact table (~350 slots) does not age out old contacts, so
once full it drops new nodes and emits CONTACTS_FULL — new contacts never
replace stale ones. The bot now evicts the stalest contacts FROM THE RADIO
to keep free headroom: at startup, automatically on CONTACTS_FULL, and
on-demand from the web UI Manage->Radio page. The bot's own sqlite
contacts table remains the untouched long-term archive.

Eviction operates on a fresh get_contacts(lastmod=0) dump (the DB archive
may hold contacts no longer on the radio, so it can't pick victims) and
never deletes DB rows. Victims are the stalest by a fused key: bot-DB
last_synced_at, then radio lastmod, then a plausible last_advert.
Protected: configured owners, all bot users, anyone DM'd in the last 24h,
and any contact types in radio_evict_protect_types. Removal is capped per
run and aborts after 3 consecutive radio errors; the lib's contact cache
is kept in step so send_dm doesn't use an evicted entry.

- [bot] config: radio_evict_enabled (default true), radio_evict_headroom
  (8), radio_evict_protect_types (default none), radio_evict_max_per_run
  (50), radio_evict_min_interval (120s debounce for auto runs).
- mcbot.py: select_eviction_victims (pure), evict_radio_contacts,
  _on_contacts_full (gated + debounced), _on_contact_deleted, startup
  hook, subscriptions, parse_contact_types helper.
- management.py: radio_contacts_status (read-only; includes the raw
  firmware autoadd byte for diagnostics), radio_evict_contacts (audited),
  radio_set_evict_policy (runtime-only; conf is the startup truth).
- webapi: GET /api/radio/contacts-status, POST /api/radio/evict-contacts,
  POST /api/radio/contacts-policy (auth required).
- web-ui: "Contact table" card in the Radio tab (status, policy toggle +
  headroom, dry-run-preview + confirm "Evict now"); rebuilt static assets.
- tests/test_radio_eviction.py: victim selection, protection, garbage
  timestamps, dry-run, headroom math, per-run cap, error-abort, shortfall,
  archive preservation, disabled-gate.

The firmware-native set_autoadd_config is intentionally NOT exposed (its
flag byte is undocumented); only a read-only autoadd diagnostic is shown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sg
sg force-pushed the fix/contacts-rollover branch from 371694b to 3117e3d Compare June 11, 2026 02:47
@sg
sg merged commit 8161780 into main Jun 11, 2026
1 check passed
@sg
sg deleted the fix/contacts-rollover branch June 12, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant