feat: Dead Drop / Mailbox auto-responder (async per-source message store)#3538
Conversation
|
I'm curious, why did you implement this directly in-tree and not as a plugin/script ? |
Yeraze
left a comment
There was a problem hiding this comment.
Automated code review (high-effort, 8 finder angles + verification). Ranked most→least severe. 🔴 #1 (message loss) and #2 (unbounded dead_drop_messages growth) I'd treat as blockers; 🟠 #3–#5 are functional bugs; 🟡 #6–#8 hardening; 🟢 #9–#10 altitude/cleanup. Per-source scoping, the migration recipe (count + last-name), the raw-SQL ban, and settings-key registration all check out — no convention violations. Nice tests.
| } | ||
| return; | ||
|
|
||
| } else if (trigger.responseType === 'mailbox') { |
There was a problem hiding this comment.
🟢 5th near-duplicate response branch (cleanup/altitude).
The enqueue loop + verifyResponse ? 3 : 1 + cooldown-set + start/duration logging are copy-pasted from the http/script/traceroute/text branches. Five copies must be kept in sync. Consider extracting a shared enqueueAutoResponderReplies(lines, message, packetId, trigger, triggerIdx) helper.
Minor: mailboxTarget re-inlines !hex formatting without the >>> 0 unsigned coerce that deadDropService.nodeIdHex uses (cosmetic — log-only here).
There was a problem hiding this comment.
I believe that the enqueue loop + verifyResponse ? 3 : 1 + cooldown-set + start/duration logging would be out-of-scope for this PR as it is effectively a 5-branch refactor. I am more than happy to help address this, but I believe that this should be split into a separate pathway as this is an overarching system optimization (scope creep on this PR)
|
@Yeraze Fair question. Honestly, the script path is reasonable. The first version I built of this was a Python script on the existing responseType: script hook. I moved it in-tree for a few things scripts can't really cover:
That said, if you'd rather keep core lean, I'm glad to repackage it as a maintained script/companion instead. Is there a plugin architecture you have in mind beyond the script responseType? Happy to go whichever way you prefer before I work through the review items. |
|
I did consider building this as a UI module/tile (the way the Solar Monitoring report is a card under Analysis & Reports) so there'd be a browser-based way to read your mailbox (out-of-band from receiving it over Meshtastic itself). But since a mailbox isn't really an analytics or reports feature, I figured tying message handling into the existing Auto Responder was a good first step. I do still plan some version of a recipient-facing mailbox UI as a companion, but as a later/bolt-on release — partly because of the access model and privacy side of it. Recipients are often node operators who don't have a MeshMonitor login, so a browser view would need an out-of-band way to authenticate: e.g. prove you control your node by entering a one-time code DM'd to it on the mesh, which mints a scoped, mailbox-only session (or lets you set up a trusted login just for the mailbox). That's enough design surface on its own that I deliberately kept it out of this PR. |
11efbb2 to
fe3177c
Compare
|
Thanks for the detailed review — pushed fixes for all 10 (
Replying inline on the threads too. |
Re-review of the latest commit (
|
A per-source 'mesh voicemail': a node DMs the radio `msg <name> <text>`
and the message is held until the named recipient retrieves it via
`inbox` / `inbox play`. Implemented as a new auto-responder
responseType ('mailbox'), reusing the existing DM-gating, per-node
cooldown, param extraction, and per-source scoping.
- DB: dead_drop_messages table (SQLite/PostgreSQL/MySQL) + migration 092
- Repository (Drizzle-only) + DatabaseService.deadDrop accessor
- DeadDropService: command brain (store/inbox/play[/sender]/delete/clear,
180-byte cap, per-recipient & per-sender caps, 7-day expiry, batch play)
- meshtasticManager: 'mailbox' responseType dispatch branch
- UI: 'Mailbox' response type option + guidance in the auto-responder editor
- Tests: 34 (migration registry, repository perSource, service)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Mailbox response type was only added to the per-trigger edit view (TriggerItem); the separate Add-Trigger form in AutoResponderSection had its own type <select> (Text/HTTP/Script) that was missed, so new mailbox triggers couldn't be created from the UI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Add button's disabled gate required a non-empty response field for all types; Mailbox has no response, so the button stayed greyed out. Exempt mailbox from the response-required check (matches validateResponse). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The settings-save validator required a non-empty response for every trigger and only allowed text/http/script responseTypes, so saving a Mailbox trigger failed with a generic 400. Exempt mailbox from the response-required check and add it to the responseType allowlist. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nse text Regression coverage for the mailbox responseType in the autoResponderTriggers validator: a mailbox trigger with empty response now saves (200), while non-mailbox empty responses and unknown responseTypes still 400. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mailbox service parsed hardcoded msg/inbox, but a trigger configured with prefixed keywords (e.g. betamsg/betainbox, to coexist with another responder already using msg/inbox) would fire the handler yet fall through to help. Strip an optional non-space prefix from the leading verb so prefixed keywords parse correctly; no-op for plain msg/inbox. Caught by live over-the-air testing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- automation.md: Mailbox response type + 'Mailbox (Dead Drop)' section (commands, recipient matching, delivery format, limits, command-prefix coexistence, configuration). - dev-notes/DEAD_DROP_TESTING.md: architecture, automated coverage, and the over-the-air validation results (ALTO MF / ALTO LF / ZN Office). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cross-database migrate-db CLI tracks every schema table in TABLE_ORDER / SKIP_TABLES; migrationTables.test.ts fails if a new schema table isn't listed. Add dead_drop_messages to TABLE_ORDER and SOURCE_SCOPED_TABLES (it carries a sourceId, like auto_favorite_targets). Caught by the full Vitest suite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Mark messages played from the delivery-success callback, not at enqueue:
handleCommand returns { responses, playOnDelivery }; a dropped body DM now
leaves the message pending instead of losing it. (Yeraze#1)
- Wire purgeExpired into databaseMaintenanceService so expired rows are
reclaimed daily (purgeExpired returns a count). (Yeraze#2)
- Count the per-recipient cap across the recipient's identity forms via an
injected node resolver, so it can't be bypassed by addressing one node by
several name forms. (Yeraze#3)
- Mailbox bypasses the per-node cooldown (interactive flow). (Yeraze#4)
- inbox play <sender> filter matches !hex/node-num forms too. (Yeraze#5)
- Non-DM commands return [] (no unsolicited DM). (Yeraze#6)
- inbox delete returns the same response for not-yours vs non-existent ids
(no id enumeration). (Yeraze#7)
- Wrap the mailbox dispatch in try/catch like the script branch. (Yeraze#8)
- Remove the command-prefix tolerance: canonical msg/inbox only. (Yeraze#9)
- Use shared nodeIdHex (unsigned coerce) for the mailbox log target. (Yeraze#10)
Docs + tests updated to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…igger Played-state is committed from the delivery-success callback; with Verify response off (maxAttempts=1) a single unacked send could mark a voicemail played on transmit. Document enabling Verify response so undelivered bodies resurface. (PR Yeraze#3538 review follow-up) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fe3177c to
fde45d2
Compare
|
Thanks for the thorough re-review — all addressed. Pushed Merge blocker — migration collision ✅Rebased onto latest
Residual follow-ups
#10 (the 5th enqueue branch) remains the open follow-up as agreed. |
Resolve migration-number collision: renumber the dead-drop migration 094_create_dead_drop -> 095_create_dead_drop (keeping main's 094_add_meshcore_node_favorite). Update migrations.ts registry, migrations.test.ts (count 95, last=create_dead_drop), and the migration's internal Postgres/MySQL export names. meshtasticManager.ts auto-merged cleanly (main's Yeraze#3593/Yeraze#3598/Yeraze#3600 changes + the mailbox dispatch coexist). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
Bump version to 4.11.3 across all five version files and document the changes merged since 4.11.2 in the changelog. Features: - Dead Drop / Mailbox auto-responder (#3538) - MeshCore node favoriting (#3588) - FEM LNA Mode LoRa config on Device Config + Remote Admin (#3599) - MeshCore CLI bundled in the Docker image (#3587) Bug fixes: - meshcore_neighbor_info timestamp BIGINT — PG/MySQL int32 overflow crash (#3602) - downlink/uplinkEnabled proto3 boolean elision revert (#3594) - Position-history SNR for directly-heard (0-hop) nodes (#3590) - MeshCore auto-ack {SNR}/{ROUTE} tokens intermittently blank (#3589) - macOS x64 DMG re2.node arch (Intel Mac crash) (#3603) - Mesh request endpoints return 503 when disconnected (#3596) Also refreshes the CLAUDE.md version header and migration count (96). Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Adds a Dead Drop / Mailbox feature — an asynchronous, per-source message store ("mesh voicemail"). A node DMs the connected radio
msg <name> <text>; MeshMonitor holds the message until the named recipient retrieves it withinbox/inbox play. The recipient need not be online when the message is sent.It's implemented as a fifth auto-responder
responseType: 'mailbox', so it reuses the existing auto-responder machinery (DM gating, per-node cooldown, parameter extraction, message chunking, per-source scoping) rather than reinventing it. Configuration is entirely through the existing Auto Responder UI — no scripts required.Commands (DM-only)
msg <name> <text><name>(short name, long name, or!nodeid)inboxinbox playinbox play <name>inbox delete <id>inbox clearHow it fits the architecture
src/db/schema/deadDrop.ts—dead_drop_messages, all three backends (SQLite/PostgreSQL/MySQL), per-sourceId.src/server/migrations/094_create_dead_drop.ts(registered insrc/db/migrations.ts;migrate-dbTABLE_ORDER/SOURCE_SCOPED_TABLESupdated).src/db/repositories/deadDrop.ts(Drizzle only) →databaseService.deadDrop.src/server/services/deadDropService.ts— command parser/executor (repo injected for testability).src/server/meshtasticManager.ts—responseType === 'mailbox'dispatch branch.src/server/routes/settingsRoutes.ts— accepts the mailbox type (noresponsefield required).auto-responder/types.ts,TriggerItem.tsx,AutoResponderSection.tsx).Design notes:
!hex, node number) at retrieval — the DM sender context proves identity, avoiding a fragile store-time node lookup.inbox play, 20 pending per recipient/sender, 7-day expiry.msg/inboxverbs (DM-only).Testing
npm run test:runpasses with 0 failures.inbox→inbox play(header + body delivery) →inbox clear— with thedead_drop_messagesrows confirmed (created → played → deleted) at each step.Docs
docs/features/automation.md— new Mailbox (Dead Drop) section.docs/internal/dev-notes/DEAD_DROP_TESTING.md— architecture + testing brief.