Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Adding a cross-boundary feature:

**Local Linux CI (optional):** Container mode — `act:ci`, `act:tests`, `act:pr`, … (needs a Docker-compatible engine + act; Podman preferred). Host mode — `act:ci:native`, `act:tests:native`, … (no container engine). See [docs/ci-cd.md](docs/ci-cd.md). macOS/Windows packaging uses native `dist:mac` / `dist:win`. **`dist:mac`** / **`dist:mac:publish`** always run **`scripts/verify-mac-packaging.mjs`** (ZIP + DMG symlink asserts, no raw `.app` CI uploads). macOS signing env (`CSC_LINK`, `CSC_KEY_PASSWORD`, `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, `APPLE_TEAM_ID`, `CSC_IDENTITY_AUTO_DISCOVERY`) is scoped to **`macos-latest`** jobs in `release.yaml` / `build.yaml`; partial-secret validation fails the release job when `CSC_LINK` is set but notarization secrets are missing.

> **Update script sync:** When adding or removing packages from `patchedDependencies` in `pnpm-workspace.yaml`, keep `WATCH_ENTRIES` in `scripts/update.sh` in sync so the script warns on version changes to every patched dependency. When adding or removing Ratspeak overlays under `reticulum-sidecar/patches/`, keep `RATSPEAK_PATCH_ENTRIES` in `scripts/update.sh` (`check_ratspeak_patches`) in sync — `pnpm run update` queries upstream PRs (rsReticulum / rsLXMF) and warns when a local overlay can be removed. Open upstream feature work (e.g. [rsReticulum#26](https://github.com/ratspeak/rsReticulum/pull/26) ReplyFile, [rsLXMF#7](https://github.com/ratspeak/rsLXMF/pull/7) multi-file attachments) is carried as overlays on floated `origin/main` — never as committed SHA pins. It also runs `check_ratspeak_upstream` (watched **published** releases for rsLXST / lrgp-rs / Ratspeak vs `reviewed-ref` pins, plus new `ratspeak` org repos) — keep `RATSPEAK_RELEASE_WATCH_ENTRIES` / `RATSPEAK_KNOWN_ORG_REPOS` in sync when adopting libs. LXMFace is not a published-release watch: its baseline is a vendored-file commit (`file:js/lxmface.js@<sha>`) compared with the latest GitHub commit that touched that file. `scripts/clone-ratspeak-stack.sh` floats **rsReticulum** / **rsLXMF** / **rsNomad** / **rsLXST** / **lrgp-rs** to `origin/main` (override with `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF` / `RS_LXST_REF` / `RS_LRGP_REF` for bisect only); overlays must apply or the clone fails. Ratspeak release watch uses stub-kind `games-parity` to nudge Games tab review when a published release is newer than the pin (`docs/reticulum-games-parity.md`). Peer default avatars use vendored **LXMFace** (`src/renderer/lib/reticulum/lxmface.ts`). `pnpm run update` also runs `rustup update` (or Homebrew `rust` on macOS without rustup) and `cargo build` in `reticulum-sidecar/` when `cargo` is on `PATH` (full-feature build includes `nomad-core` / rsNomad).
> **Update script sync:** When adding or removing packages from `patchedDependencies` in `pnpm-workspace.yaml`, keep `WATCH_ENTRIES` in `scripts/update.sh` in sync so the script warns on version changes to every patched dependency. When adding or removing Ratspeak overlays under `reticulum-sidecar/patches/`, keep `RATSPEAK_PATCH_ENTRIES` in `scripts/update.sh` (`check_ratspeak_patches`) in sync — `pnpm run update` queries upstream PRs (rsReticulum / rsLXMF) and warns when a local overlay can be removed. Open upstream feature work (e.g. [rsReticulum#26](https://github.com/ratspeak/rsReticulum/pull/26) ReplyFile, [rsLXMF#7](https://github.com/ratspeak/rsLXMF/pull/7) multi-file attachments) is carried as overlays on floated `origin/main` — never as committed SHA pins. It also runs `check_ratspeak_upstream` (watched **published** releases for rsLXST / lrgp-rs / Ratspeak vs `reviewed-ref` pins, plus new `ratspeak` org repos) — keep `RATSPEAK_RELEASE_WATCH_ENTRIES` / `RATSPEAK_KNOWN_ORG_REPOS` in sync when adopting libs. LXMFace is not a published-release watch: its baseline is a vendored-file commit (`file:js/lxmface.js@<sha>`) compared with the latest GitHub commit that touched that file. Vendored **MECP** ([xiang-dev-1/MECP](https://github.com/xiang-dev-1/MECP)) is watched the same way via `MECP_UPSTREAM_WATCH_ENTRIES` / `check_mecp_upstream` (`file:engine/src@<sha>` and `file:languages@<sha>` vs `src/renderer/lib/mecp/`). `scripts/clone-ratspeak-stack.sh` floats **rsReticulum** / **rsLXMF** / **rsNomad** / **rsLXST** / **lrgp-rs** to `origin/main` (override with `RS_RETICULUM_REF` / `RS_LXMF_REF` / `RS_NOMAD_REF` / `RS_LXST_REF` / `RS_LRGP_REF` for bisect only); overlays must apply or the clone fails. Ratspeak release watch uses stub-kind `games-parity` to nudge Games tab review when a published release is newer than the pin (`docs/reticulum-games-parity.md`). Peer default avatars use vendored **LXMFace** (`src/renderer/lib/reticulum/lxmface.ts`). `pnpm run update` also runs `rustup update` (or Homebrew `rust` on macOS without rustup) and `cargo build` in `reticulum-sidecar/` when `cargo` is on `PATH` (full-feature build includes `nomad-core` / rsNomad).

**Pre-commit hook order:**

Expand Down Expand Up @@ -156,6 +156,7 @@ Deep, file-level subsystem detail now lives in [`docs/agents/`](docs/agents/READ
| Diagnostics engines, rows, tab scoping | [`docs/agents/diagnostics.md`](docs/agents/diagnostics.md) |
| i18n / localization workflow, auto-translate, language selector | [`docs/agents/i18n.md`](docs/agents/i18n.md) |
| Connection panel helpers (error hints, rehydrate, storage migrations) | [`docs/agents/connection-panel.md`](docs/agents/connection-panel.md) |
| MECP emergency reports, siren, audit log, ALERT_APP, RF rebroadcast | [`docs/agents/mecp.md`](docs/agents/mecp.md) |
| Symptom → where-to-check index | [`docs/agents/common-issues.md`](docs/agents/common-issues.md) |

**Always-remember invariants** (details in the linked files):
Expand Down
1 change: 1 addition & 0 deletions docs/agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Deep, file-level subsystem detail for AI assistants, split out of [`AGENTS.md`](
| Diagnostics engines, rows, tab scoping | [diagnostics.md](diagnostics.md) |
| i18n / localization workflow, auto-translate, language selector | [i18n.md](i18n.md) |
| Connection panel helpers (error hints, rehydrate, storage migrations) | [connection-panel.md](connection-panel.md) |
| MECP emergency reports, siren alerts, audit log, ALERT_APP, RF rebroadcast | [mecp.md](mecp.md) |
| Symptom → where-to-check index | [common-issues.md](common-issues.md) |

For human-facing deep dives, see the top-level docs (e.g. [../reticulum.md](../reticulum.md), [../diagnostics.md](../diagnostics.md), [../meshcore-meshtastic-parity.md](../meshcore-meshtastic-parity.md), [../troubleshooting.md](../troubleshooting.md)).
2 changes: 1 addition & 1 deletion docs/agents/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Deep subsystem reference for AI assistants. Open this when a task touches the Ch
- **Composer limits / send cadence:** `chatComposerLimits.ts` — `getMaxChunks(protocol)` via `ProtocolCapabilities.composerMaxChunks` (MeshCore = 1: no outbound `[i/N]` split; `splitChatMessage` returns `null` when text needs more than one packet), room payload via `getMeshcoreRoomPayloadLimit`, `computeComposerLimitStatus` phases (`warn` surfaces a single-packet ⓘ hint; `overMaxSingle` disables send and shows a `role="note"` callout). MeshCore also gets a **non-blocking** ~5s "sending too fast" advisory (`role="status"`, dismissible) from an app-wide clock in `meshcoreSendRateNotice.ts` (`recordMeshcoreSend` / `isMeshcoreSendTooFast`, `MESHCORE_FAST_SEND_WARN_INTERVAL_MS`). Every **live** MeshCore send advances the clock and can show the advisory — text (`handleSend`), GIF, and share-location in `ChatComposer`, plus outbox drain (`useChatOutbox.ts`). Legacy MeshCore outbox rows with `groupTotal > 1` or `[i/N]` payloads are quarantined (`blocked`) on drain instead of being transmitted. Inbound multi-part `[i/N]` merge is unchanged. i18n: `chatPanel.composeLimit.meshcoreSingleNotice.*`, `chatPanel.meshcoreFastSend.warning`, `chatPanel.outboxLegacyMultipartBlocked`. See [`meshcore-meshtastic-parity.md`](../meshcore-meshtastic-parity.md).
- **Payload / links:** `ChatPayloadText.tsx` — mention highlighting, search marks, URL linkification; link previews via `chat:fetchLinkPreview` (`src/main/fetchLinkPreview.ts`): Open Graph for HTML pages; **YouTube** watch/shorts/youtu.be via oEmbed + thumbnail; **direct image URLs** (path extension via `chatDirectImageUrl.ts` or raster `Content-Type`) return `kind: 'image'` and render as inline embeds (`ChatInlineImage` / `DirectImageEmbed`); OG/YouTube use card layout. Security: DNS-pinned undici `Agent`, private/loopback blocked, magic-byte MIME sniff (`safeRasterImageMime.ts`), HTTPS-only image embeds, 10s fetch / 3s DNS, 64 KiB HTML cap, **2 MiB** image fetch cap (256 KiB cache payload cap), LRU caches, single-flight dedup (renderer map capped). Previews load even when scrolled up. LXMF attachment rasters: `chat:readReticulumAttachmentAsDataUrl` (`reticulum-attachment-image.ts`; path jail, magic-byte MIME, SVG rejected, 2 MiB, IPC rate limit) → `ReticulumAttachmentLine`. LXMF voice memos (`hasReticulumVoiceMemo`): Chat DM mic → sidecar `/api/v1/voice/memo/*` via `electronAPI.reticulum.voiceMemo.*`; playback via `chat:readReticulumAttachmentBytes` (`reticulum-attachment-audio.ts`; OggS sniff, 256 KiB) → `ReticulumVoiceMemoLine`. Reply quotes: `replyPreview.ts`.
- **Storage helpers:** `src/renderer/lib/chatPanelProtocolStorage.ts` — drafts (`mesh-client:drafts:<protocol>`), open DM tabs, last-read, per-view mute (`mesh-client:mutedViews:<protocol>`), starred (`mesh-client:starred:<protocol>`, cap 200), MeshCore flood-scope overrides per chat view (`mesh-client:floodScopeOverrides:<protocol>`, channel or DM `viewKey`).
- **Notifications:** `src/renderer/lib/chatNotifications.ts` — `playMessageNotification(type)` via Web Audio: `channel` = single 880 Hz pulse (150 ms); `dm` / `reply` = dual pulse (587.33 Hz then 783.99 Hz, 50 ms each, 35 ms gap). Resumes suspended `AudioContext` when the window is hidden/minimized. Type selection in `chatUnreadCounts.ts` (`resolveChatNotificationType`, `pickAudibleNotificationType`; batch priority reply > dm > channel). **ChatPanel** plays when the user is on Chat but reading another view; **App** plays for other panels / backgrounded window (avoids double beep). Meshtastic hidden-window desktop notifications are visual-only (`silent: true` in `meshtasticRouterSideEffects.ts`); typed Web Audio from App owns sound. Global mute `mesh-client:notifMuted`; per-view mute in `mutedViews`. Main-process **tray** icon shows unread when chat or MeshCore Rooms traffic arrives while backgrounded (`src/main/index.ts` `buildTrayIcon`).
- **Notifications:** `src/renderer/lib/chatNotifications.ts` — `playMessageNotification(type)` via Web Audio: `channel` = single 880 Hz pulse (150 ms); `dm` / `reply` = dual pulse (587.33 Hz then 783.99 Hz, 50 ms each, 35 ms gap); **`mecp`** = triple ascending pulse; **`mecpSiren`** = loud multi-cycle siren for MECP severity 0–1 (mute-bypass via `useMecpAlertWatcher` / `mecpAlert.ts`, not `pickAudibleNotificationType`). Resumes suspended `AudioContext` when the window is hidden/minimized. Type selection in `chatUnreadCounts.ts` (`resolveChatNotificationType`, `pickAudibleNotificationType`; batch priority reply > dm > channel). **ChatPanel** plays when the user is on Chat but reading another view; **App** plays for other panels / backgrounded window (avoids double beep). Meshtastic hidden-window desktop notifications are visual-only (`silent: true` in `meshtasticRouterSideEffects.ts`); typed Web Audio from App owns sound. Global mute `mesh-client:notifMuted`; per-view mute in `mutedViews`. Main-process **tray** icon shows unread when chat or MeshCore Rooms traffic arrives while backgrounded (`src/main/index.ts` `buildTrayIcon`). MECP compose/export and red-dashed bubbles: see [`mecp.md`](mecp.md).
- **Meshtastic dedup:** `meshtasticMessageDedup.ts` — merges delayed RF/MQTT duplicates (**10-minute** content window) in `useMeshtasticRuntime` ingest.
- **Hop badges:** `MessageRecord.rxHops` / `viaStoreForward` round-trip via `storeRecordAdapters.ts` and `meshtasticDbCacheHydration.ts` (`hopCount` on `MessageRecord` bridges to `rxHops` for Meshtastic PacketRouter rows); Chat hop pills read `ChatMessage.rxHops`. **MeshCore (primary):** companion `pathLen` on events 7/8 → `meshcoreCompanionRxPathLenToHopCount` → `DomainEvent.payload.hopCount` (`MeshCoreProtocol`, `meshcoreDirectMessageDecode`); waiting-message drain also sets `rxHops` from `pathLen`. **MeshCore (fallback):** raw-log correlation via `resolveMeshcoreIngestRxHops` / `MESHCORE_CHAT_CORRELATE_WINDOW_MS` (3000ms); `rawPacketsRef` synced inside event-136 `setRawPackets` updater for same-tick ingest (`meshcoreConnSideEffects`). **Meshtastic:** `MeshtasticProtocol` uses `meshtasticComputedRfHopsAway` — omit hops for `viaMqtt` and `hopStart === 0`; else `hopStart - hopLimit` when `hopStart > 0 && hopLimit <= hopStart`.
- **Relay coverage (in-memory, outgoing bubbles only):** `relayCoverageStore` keyed by `identityId:messageId` (not persisted). UI: `RelayCoverageLine` in `ChatPanel` status row via `relayCoverageMessageKey`. **MeshCore:** `openHeardRepeatWindow` from Chat `useSendMessage` channel sends and runtime channel TX; during `MESHCORE_HEARD_REPEAT_WINDOW_MS` (**120s** — large meshes often need ≥60s for multi-hop returns), every overheard `GRP_TXT` flood-path segment credits a forwarder (Repeater/Room when resolved via `pubKeyMapRef` / pubkey prefix; otherwise synthetic hex as “additional unidentified”). Chat hops are never credited. Path-invariant payload id (`meshCorePathInvariantPayloadId`: CRC lookup + full type/payload equality) binds only from **own-TX** or **empty-path** channel echo (never from the first credited hop); once bound, mismatched floods are ignored — unbound windows credit all in-window forwarders (pre-#888 best-effort). Naming is reliable on **2/3-byte** path modes; **1-byte** is best-effort (collisions common). Only forwarders whose rebroadcast reaches our radio are credited (multi-hop peers appear via accumulated path hashes). `renameMessageId` re-keys coverage + window; disconnect clears window + identity coverage. **Meshtastic:** binary heard/timeout for RF channel/broadcast via `meshtasticHeardRepeat` + `useMeshtasticRuntime` transport status (DMs/MQTT ignored). **Reticulum:** predicted route at send (`reticulumRouteCoverage` / `sendReticulumChatMessage`); hops-only vs via-first-hop i18n; pending→hash rename re-keys; `clearReticulumSessionStores` strips reticulum coverage. i18n: `chatPanel.heardBy*`, `chatPanel.route*Predicted*`.
Expand Down
59 changes: 59 additions & 0 deletions docs/agents/mecp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Agent reference: MECP (Mesh Emergency Communication Protocol)

Deep subsystem reference for AI assistants. Open when a task touches MECP compose, alerts, audit log, ALERT_APP ingest, or cross-protocol RF rebroadcast. Hard rules live in [`AGENTS.md`](../../AGENTS.md).

## Wire format

```
MECP/<severity>/<codes> [freetext]
```

- Severity: `0` MAYDAY, `1` URGENT, `2` SAFETY, `3` ROUTINE
- Codes: letter + two digits (`M01`, …); drill `D01`/`D02` set `isDrill` (suppresses alerts)
- Max **200** UTF-8 bytes (`MAX_MESSAGE_BYTES`)
- Vendored engine: [`src/renderer/lib/mecp/engine/`](../../src/renderer/lib/mecp/engine/) from [xiang-dev-1/MECP](https://github.com/xiang-dev-1/MECP) (GPLv3)
- Language packs: [`src/renderer/lib/mecp/languages/`](../../src/renderer/lib/mecp/languages/) (CC BY 4.0)
- Overview / video: [mecp.radio](https://mecp.radio/)
- App wrappers: `mecpMessages.ts` (`MECP_REGEX`, `tryParseMecp`), `mecpAlert.ts`, `mecpRebroadcast.ts`

## Receive path

1. Messages land in `messageStore` as normal chat text (Meshtastic RF/MQTT, MeshCore, Reticulum).
2. Meshtastic **`ALERT_APP`** (port 11) is decoded like `TEXT_MESSAGE_APP` in `MeshtasticProtocol` / MQTT.
3. `useMecpAlertWatcher` (mounted once from `App.tsx`):
- Seeds a dedup set at mount (no alert/audit on hydration)
- New inbound MECP → durable audit append (`mecp:appendReceived`)
- Alerts: sev **0** `'mecpSiren'` + emergency toast; sev **1** US EAS-style 853+960 Hz `'mecpEas'` + toast (**ignore** mutes; **always** including focused chat); sev **2–3** loud repeated `'mecp'` burst when unmuted; drills never alert
- Focused Chat still alerts via `ChatPanel` → `triggerMecpAlert` (deduped with the watcher)
- Optional RF rebroadcast (§ below)

## Durable audit log

- File: `mecp-received.log` (+ `.1` size rotate) under Electron `userData` — **not** session `mesh-client.log`
- IPC: `mecp:appendReceived`, `mecp:exportReceivedLog` (Save dialog)
- Included in support bundles
- App → MECP section: **Export MECP log** (Save dialog) alongside RF bridge settings and a link to upstream docs

## Send path

- App → MECP → **Show MECP button in Chat** (default **off**) gates the Chat compose control
- When enabled: Chat **MECP** button → `MecpComposeModal` (defaults: ROUTINE + Drill category, no codes selected) → encode → existing `handleSendChunk` / `useSendMessage` (follows open DM/channel)
- Attach GPS uses the app share-location waterfall (`resolveShareLocation`), not raw `navigator.geolocation` alone
- Meshtastic outbound uses normal text (`TEXT_MESSAGE_APP`), not ALERT_APP

## RF rebroadcast (default off)

- App → MECP section: rules `{ enabled, bidirectional, endpointA, endpointB }` (Meshtastic/MeshCore channel indices 0–7)
- One-way **A→B** by default; **Bidirectional** toggle enables B→A
- Trigger: new inbound MECP with `receivedVia` `rf`/`both` (not mqtt-only); skip own/history/drill
- Loop guard: payload+dest fingerprint TTL
- After each successful bridge send: short follow-up notice `MECP from <sender> via <Meshtastic|MeshCore> (<channel name>)` (not a wire MECP; channel **name**, not index)
- Implementation: `mecpRebroadcast.ts` + `sendMecpRebroadcast.ts`

## Out of scope (follow-ups)

- RetAlert (`!RETALERT!…`)
- SQLite `mecpParsed` column / in-memory emergency panel
- MeshCore Rooms bubble styling
- Send via `ALERT_APP` portnum
- Reticulum DM bridge endpoints
Loading
Loading