diff --git a/AGENTS.md b/AGENTS.md index dfb3d08..f8aa473 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -165,7 +165,8 @@ MeshCore firmware can encode path hops as 1-byte, 2-byte, or 3-byte identifiers. Direct-message send behavior intentionally mirrors the firmware/library `send_msg_with_retry(...)` flow: - We push the contact's effective route to the radio via `add_contact(...)` before sending. -- Non-final attempts use the effective route (`override > direct > flood`). +- If the initial `MSG_SENT` result includes an expected ACK code, background retries are armed. +- Non-final retry attempts use the effective route (`override > direct > flood`). - Retry timing follows the radio's `suggested_timeout`. - The final retry is sent as flood by resetting the path on the radio first, even if an override or direct route exists. - Path math is always hop-count based; hop bytes are interpreted using the stored `path_hash_mode`. @@ -174,7 +175,7 @@ Direct-message send behavior intentionally mirrors the firmware/library `send_ms **Direct messages**: Expected ACK code is tracked. When ACK event arrives, message marked as acked. -Outgoing DMs send once immediately, then may retry up to 2 more times in the background if still unacked. Retry timing follows the radio's `suggested_timeout` from `PACKET_MSG_SENT`, and the final retry is sent as flood even when a routing override is configured. DM ACK state is terminal on first ACK: sibling retry ACK codes are cleared so one DM should not accumulate multiple delivery confirmations from different retry attempts. +Outgoing DMs send once immediately, then may retry up to 2 more times in the background only when the initial `MSG_SENT` result includes an expected ACK code and the message remains unacked. Retry timing follows the radio's `suggested_timeout` from `PACKET_MSG_SENT`, and the final retry is sent as flood even when a routing override is configured. DM ACK state is terminal on first ACK: sibling retry ACK codes are cleared so one DM should not accumulate multiple delivery confirmations from different retry attempts. ACKs are not a contact-route source. They drive message delivery state and may appear in analytics/detail surfaces, but they do not update `direct_path*` or otherwise influence route selection for future sends. @@ -324,6 +325,7 @@ All endpoints are prefixed with `/api` (e.g., `/api/health`). | POST | `/api/contacts/{public_key}/command` | Send CLI command to repeater | | POST | `/api/contacts/{public_key}/routing-override` | Set or clear a forced routing override | | POST | `/api/contacts/{public_key}/trace` | Trace route to contact | +| POST | `/api/contacts/{public_key}/path-discovery` | Discover forward/return paths and persist the learned direct route | | POST | `/api/contacts/{public_key}/repeater/login` | Log in to a repeater | | POST | `/api/contacts/{public_key}/repeater/status` | Fetch repeater status telemetry | | POST | `/api/contacts/{public_key}/repeater/lpp-telemetry` | Fetch CayenneLPP sensor data | @@ -348,7 +350,7 @@ All endpoints are prefixed with `/api` (e.g., `/api/health`). | GET | `/api/packets/undecrypted/count` | Count of undecrypted packets | | POST | `/api/packets/decrypt/historical` | Decrypt stored packets | | POST | `/api/packets/maintenance` | Delete old packets and vacuum | -| GET | `/api/read-state/unreads` | Server-computed unread counts, mentions, last message times | +| GET | `/api/read-state/unreads` | Server-computed unread counts, mentions, last message times, and `last_read_ats` boundaries | | POST | `/api/read-state/mark-all-read` | Mark all conversations as read | | GET | `/api/settings` | Get app settings | | PATCH | `/api/settings` | Update app settings | @@ -398,7 +400,7 @@ Read state (`last_read_at`) is tracked **server-side** for consistency across de - Stored as Unix timestamp in `contacts.last_read_at` and `channels.last_read_at` - Updated via `POST /api/contacts/{public_key}/mark-read` and `POST /api/channels/{key}/mark-read` - Bulk update via `POST /api/read-state/mark-all-read` -- Aggregated counts via `GET /api/read-state/unreads` (server-side computation) +- Aggregated counts via `GET /api/read-state/unreads` (server-side computation of counts, mention flags, `last_message_times`, and `last_read_ats`) **State Tracking Keys (Frontend)**: Generated by `getStateKey()` for message times (sidebar sorting): - Channels: `channel-{channel_key}` diff --git a/app/AGENTS.md b/app/AGENTS.md index be2d2d0..f87c8a8 100644 --- a/app/AGENTS.md +++ b/app/AGENTS.md @@ -113,16 +113,16 @@ app/ ### Read/unread state - Server is source of truth (`contacts.last_read_at`, `channels.last_read_at`). -- `GET /api/read-state/unreads` returns counts, mention flags, and `last_message_times`. +- `GET /api/read-state/unreads` returns counts, mention flags, `last_message_times`, and `last_read_ats`. ### DM ingest + ACKs - `services/dm_ingest.py` is the one place that should decide fallback-context resolution, DM dedup/reconciliation, and packet-linked vs. content-based storage behavior. - `CONTACT_MSG_RECV` is a fallback path, not a parallel source of truth. If you change DM storage behavior, trace both `event_handlers.py` and `packet_processor.py`. - DM ACK tracking is an in-memory pending/buffered map in `services/dm_ack_tracker.py`, with periodic expiry from `radio_sync.py`. -- Outgoing DMs send once inline, store/broadcast immediately after the first successful `MSG_SENT`, then may retry up to 2 more times in the background if still unacked. +- Outgoing DMs send once inline, store/broadcast immediately after the first successful `MSG_SENT`, then may retry up to 2 more times in the background only when the initial `MSG_SENT` result includes an expected ACK code and the message remains unacked. - DM retry timing follows the firmware-provided `suggested_timeout` from `PACKET_MSG_SENT`; do not replace it with a fixed app timeout unless you intentionally want more aggressive duplicate-prone retries. -- Direct-message send behavior is intended to emulate `meshcore_py.commands.send_msg_with_retry(...)`: stage the effective contact route on the radio, send, wait for ACK, and on the final retry force flood via `reset_path(...)`. +- Direct-message send behavior is intended to emulate `meshcore_py.commands.send_msg_with_retry(...)` when the radio provides an expected ACK code: stage the effective contact route on the radio, send, wait for ACK, and on the final retry force flood via `reset_path(...)`. - Non-final DM attempts use the contact's effective route (`override > direct > flood`). The final retry is intentionally sent as flood even when a routing override exists. - DM ACK state is terminal on first ACK. Retry attempts may register multiple expected ACK codes for the same message, but sibling pending codes are cleared once one ACK wins so a DM should not accrue multiple delivery confirmations from retries. - ACKs are delivery state, not routing state. Bundled ACKs inside PATH packets still satisfy pending DM sends, but ACK history does not feed contact route learning. @@ -188,6 +188,7 @@ app/ - `POST /contacts/{public_key}/command` - `POST /contacts/{public_key}/routing-override` - `POST /contacts/{public_key}/trace` +- `POST /contacts/{public_key}/path-discovery` — discover forward/return paths, persist the learned direct route, and sync it back to the radio best-effort - `POST /contacts/{public_key}/repeater/login` - `POST /contacts/{public_key}/repeater/status` - `POST /contacts/{public_key}/repeater/lpp-telemetry` @@ -219,7 +220,7 @@ app/ - `POST /packets/maintenance` ### Read state -- `GET /read-state/unreads` +- `GET /read-state/unreads` — counts, mention flags, `last_message_times`, and `last_read_ats` - `POST /read-state/mark-all-read` ### Settings diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index d92a280..ac50444 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -198,9 +198,9 @@ High-level state is delegated to hooks: - `useContactsAndChannels`: contact/channel lists, creation, deletion - `useConversationRouter`: URL hash → active conversation routing - `useConversationNavigation`: search target, conversation selection reset, and info-pane state -- `useConversationActions`: send/resend/trace/block handlers and channel override updates +- `useConversationActions`: send/resend/trace/path-discovery/block handlers and channel override updates - `useConversationMessages`: conversation switch loading, embedded conversation-scoped cache, jump-target loading, pagination, dedup/update helpers, reconnect reconciliation, and pending ACK buffering -- `useUnreadCounts`: unread counters, mention tracking, recent-sort timestamps +- `useUnreadCounts`: unread counters, mention tracking, recent-sort timestamps, and server `last_read_ats` boundaries - `useRealtimeAppState`: typed WS event application, reconnect recovery, cache/unread coordination - `useRepeaterDashboard`: repeater dashboard state (login, pane data/retries, console, actions) @@ -331,6 +331,8 @@ Note: MQTT, bot, and community MQTT settings were migrated to the `fanout_config `RawPacket.decrypted_info` includes `channel_key` and `contact_key` for MQTT topic routing. +`UnreadCounts` includes `counts`, `mentions`, `last_message_times`, and `last_read_ats`. The unread-boundary/jump-to-unread behavior uses the server-provided `last_read_ats` map keyed by `getStateKey(...)`. + ## Contact Info Pane Clicking a contact's avatar in `ChatHeader` or `MessageList` opens a `ContactInfoPane` sheet (right drawer) showing comprehensive contact details fetched from `GET /api/contacts/analytics` using either `?public_key=...` or `?name=...`: